Compare commits

...

257 Commits

Author SHA1 Message Date
Sofia Rodrigues
64889857b2 Merge branch 'master' of https://github.com/leanprover/lean4 into sofia/async-http-uri 2026-03-19 15:42:35 -03:00
Sofia Rodrigues
33caa4e82f fix: test 2026-03-17 12:03:35 -03:00
Sofia Rodrigues
8c292c70ee Merge branch 'master' of https://github.com/leanprover/lean4 into sofia/async-http-uri 2026-03-17 10:42:30 -03:00
Sofia Rodrigues
4f4ee7c789 Merge branch 'master' of https://github.com/leanprover/lean4 into sofia/async-http-uri 2026-03-17 10:42:19 -03:00
Sofia Rodrigues
d7ea3a5984 fix: redundant end and namespace 2026-03-17 10:23:17 -03:00
Sofia Rodrigues
33c36c7466 fix: absolute-form parse and add helper functions 2026-03-17 10:17:00 -03:00
Sofia Rodrigues
72244398dc fix: test 2026-03-12 15:38:59 -03:00
Sofia Rodrigues
400908a2f4 fix: port and fragment 2026-03-12 15:09:49 -03:00
Sofia Rodrigues
394c999c2a fix: uri 2026-03-12 15:03:49 -03:00
Sofia Rodrigues
b7e88dadeb Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-03-12 15:01:03 -03:00
Sofia Rodrigues
a39a0575a0 Merge branch 'master' into sofia/async-http-headers 2026-03-12 14:35:25 -03:00
Sofia Rodrigues
5815f33342 Merge branch 'sofia/fix-native-decide' into sofia/async-http-headers 2026-03-12 14:17:13 -03:00
Sofia Rodrigues
7a852aedb6 fix: squeeze simp and paren 2026-03-10 10:08:22 -03:00
Sofia Rodrigues
1554f57525 fix: import 2026-03-09 21:18:57 -03:00
Sofia Rodrigues
1fa01cdadb style: just removed variable 2026-03-09 20:35:53 -03:00
Sofia Rodrigues
758e5afb07 refactor: simplify 2026-03-09 20:35:53 -03:00
Sofia Rodrigues
11516bbf09 fix: import 2026-03-09 20:35:53 -03:00
Sofia Rodrigues
f76dca5bba fix: proof 2026-03-09 20:35:53 -03:00
Sofia Rodrigues
fe6ac812af fix: panic 2026-03-09 20:35:53 -03:00
Sofia Rodrigues
51a00843ea fix: remove usage of 2026-03-09 20:35:53 -03:00
Sofia Rodrigues
cfe282f024 fix: port parse 2026-03-04 16:40:03 -03:00
Sofia Rodrigues
2668f07808 fix: alpha and isdigit 2026-03-04 16:29:31 -03:00
Sofia Rodrigues
b9e489cc8f Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-03-04 16:22:58 -03:00
Sofia Rodrigues
135b049080 Merge branch 'master' into sofia/async-http-headers 2026-03-04 13:22:45 -03:00
Sofia Rodrigues
4005bd027b fix: size 2026-03-04 12:04:53 -03:00
Sofia Rodrigues
fbf03e31f9 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-03-04 10:21:46 -03:00
Sofia Rodrigues
39ab2b289c test: move test from encode to the uri tests 2026-03-04 10:20:25 -03:00
Sofia Rodrigues
6c6f9a5d83 refactor: change Char.isDigit and Char.isAlpha 2026-03-04 10:00:41 -03:00
Sofia Rodrigues
a7aea9a12d style: format 2026-03-04 09:50:30 -03:00
Sofia Rodrigues
3dfb5e002a Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-03-03 13:59:01 -03:00
Sofia Rodrigues
3075e5091b Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-03-03 13:58:55 -03:00
Sofia Rodrigues
af12f7e9be revert: wrong comment 2026-03-03 13:58:08 -03:00
Sofia Rodrigues
cfa5cf76fc Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-03-03 12:42:23 -03:00
Sofia Rodrigues
238925a681 style: change field names 2026-03-03 12:38:52 -03:00
Sofia Rodrigues
8cb236e9eb style: remove parenthesis 2026-03-03 12:35:29 -03:00
Sofia Rodrigues
3d039f8dba fix: bugs and code style 2026-03-03 12:34:12 -03:00
Sofia Rodrigues
203d5362d4 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-03-03 12:12:38 -03:00
Sofia Rodrigues
6189d4c130 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-03-03 12:10:31 -03:00
Sofia Rodrigues
58f14d34d7 chore: improve comments 2026-03-03 12:10:22 -03:00
Sofia Rodrigues
710eee2b49 refactor: head to line 2026-03-03 12:08:26 -03:00
Sofia Rodrigues
bd4af50d04 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-03-03 12:03:55 -03:00
Sofia Rodrigues
8cb30347b6 fix: rename head to line 2026-03-03 12:03:47 -03:00
Sofia Rodrigues
d8e6b09b90 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-03-03 11:58:30 -03:00
Sofia Rodrigues
df8abc2b3f fix: remove token let tchar 2026-03-03 11:58:21 -03:00
Sofia Rodrigues
5a852bdffd Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-03-03 11:57:32 -03:00
Sofia Rodrigues
11d3860c69 fix: remove char testBit 2026-03-03 11:57:20 -03:00
Sofia Rodrigues
5a253001b3 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-03-03 11:39:12 -03:00
Sofia Rodrigues
083fec29c8 test: fix 2026-03-03 11:38:54 -03:00
Sofia Rodrigues
d41753a5f9 fix: suggestions 2026-03-03 10:11:25 -03:00
Sofia Rodrigues
a086a817e0 feat: v10 2026-03-03 09:26:45 -03:00
Sofia Rodrigues
67822f4c42 refactor: remove bv 2026-03-03 00:36:38 -03:00
Sofia Rodrigues
f85b9b8d09 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-03-03 00:34:21 -03:00
Sofia Rodrigues
5fb254b7ef Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-03-03 00:24:40 -03:00
Sofia Rodrigues
6e202e34a4 feat: all char predicates 2026-03-03 00:24:16 -03:00
Sofia Rodrigues
843c814778 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-03-03 00:18:49 -03:00
Sofia Rodrigues
c7d4d8d799 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-03-03 00:17:31 -03:00
Sofia Rodrigues
91c60f801c fix: rstore treeMap tests from master 2026-03-03 00:16:26 -03:00
Sofia Rodrigues
ae30f55728 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-03-02 20:52:24 -03:00
Sofia Rodrigues
63b0cc17c4 fix: char predicates 2026-03-02 20:51:55 -03:00
Sofia Rodrigues
7434b97511 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-03-02 19:47:48 -03:00
Sofia Rodrigues
29c8f8cfa1 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-03-02 19:44:03 -03:00
Sofia Rodrigues
36b2d99e3d fix: encode 2026-03-02 19:43:57 -03:00
Sofia Rodrigues
b8f2cd94aa Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-03-02 12:00:58 -03:00
Sofia Rodrigues
64ff045559 fix: remove treemap 2026-03-02 12:00:55 -03:00
Sofia Rodrigues
109ab8eb68 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-03-02 11:59:21 -03:00
Sofia Rodrigues
bf09ea8ff5 feat: remove tests temporarily 2026-03-02 11:56:45 -03:00
Sofia Rodrigues
7ce9fe9f97 feat: remove tests temporarily 2026-03-02 11:54:16 -03:00
Sofia Rodrigues
aff9e0c459 refactor: rust-types-rs like method enum with IANA specification 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
a74df33feb fix: method 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
dd63b614eb fix: comments 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
515e6e20c0 fix: test 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
cc45fc9cc2 fix: dots 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
bc9c18f0b0 fix: small changes 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
8ee21a7176 fix: comment 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
92aa9f2b8a fix: RFC checks and small improvements 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
c2243a0ea5 fix: tests 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
efbd23a6d9 fix: format 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
26440fcf6a fix: extension values 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
ac4c5451e4 fix: data char 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
c94c5cb7e4 fix: comments 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
78ca6edc99 feat: specialize quote 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
d92dc22df3 fix: test 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
48ab74f044 fix: status code 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
da68a63902 feat: reason phrase in custom status code 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
db99fd2d7d feat: ignore reasonphrase 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
a61712c962 feat: validation in reasonPhrase 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
ea36555588 fix: reasonPhrase 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
b02bc4d6d2 feat: reason phrase 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
c836fe8723 fix: typos and compareName 2026-03-02 11:53:56 -03:00
Sofia Rodrigues
8068ed317c fix: typos 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
0bd44ab745 fix: comment 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
172d12c75c refactor: move trailers 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
6b6b9fffff feat: add extension handling of quotes and ExtensionName 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
f3fa5c8242 fix: chunked 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
b0c5667f06 fix: import 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
2d262c9755 fix: interpolation 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
571898bf63 fix: extensions 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
0570277a2e feat: add extensions 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
557709d9bb fix: apply suggestions 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
0229508ca7 refactor: remove headers 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
ace10ee42b fix: default size 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
4e36dcc98f fix: apply suggestions
Co-authored-by: Rob23oba <152706811+Rob23oba@users.noreply.github.com>
2026-03-02 11:53:55 -03:00
Sofia Rodrigues
a93ea184fe fix: status and chunkedbuffer 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
c309a3c07e feat: basic headers structure to more structured approach 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
30641c617f feat: data components 2026-03-02 11:53:55 -03:00
Sofia Rodrigues
0bb4ba72d4 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-28 15:41:11 -03:00
Sofia Rodrigues
57a4d9ad4b Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-28 15:41:04 -03:00
Sofia Rodrigues
bfc6617c12 fix: method 2026-02-28 15:40:55 -03:00
Sofia Rodrigues
3aa02eede3 fix: comments 2026-02-27 14:31:09 -03:00
Sofia Rodrigues
c86f926d1b Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-27 14:22:38 -03:00
Sofia Rodrigues
ff4419357c Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-27 14:20:02 -03:00
Sofia Rodrigues
3c131da050 fix: comments 2026-02-27 14:19:32 -03:00
Sofia Rodrigues
6edc0c7427 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-27 13:22:51 -03:00
Sofia Rodrigues
563189fec9 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-27 13:04:51 -03:00
Sofia Rodrigues
25d7db2e62 Merge branch 'master' of https://github.com/leanprover/lean4 into sofia/async-http-data 2026-02-27 10:08:06 -03:00
Sofia Rodrigues
cdfd24171a fix: test 2026-02-26 16:15:03 -03:00
Sofia Rodrigues
718e549de3 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-26 16:10:56 -03:00
Sofia Rodrigues
81f76a24d8 fix:lower case scheme 2026-02-26 16:10:46 -03:00
Sofia Rodrigues
292f297006 feat: small comments 2026-02-26 15:41:20 -03:00
Sofia Rodrigues
b7be57272a feat: forIn 2026-02-26 15:24:17 -03:00
Sofia Rodrigues
a0dc1dbbc0 fix: test 2026-02-26 15:18:32 -03:00
Sofia Rodrigues
2e604884dd Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-26 15:16:35 -03:00
Sofia Rodrigues
2049542833 feat: order 2026-02-26 15:16:23 -03:00
Sofia Rodrigues
caf19b8458 feat: order 2026-02-26 15:13:56 -03:00
Sofia Rodrigues
c5180b2dfc Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-26 01:23:46 -03:00
Sofia Rodrigues
91c5b717f0 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-26 01:23:24 -03:00
Sofia Rodrigues
cb6f540efb fix: test 2026-02-26 01:22:46 -03:00
Sofia Rodrigues
ec833b52ee Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-26 01:01:46 -03:00
Sofia Rodrigues
ba36c1dee2 fix: comments 2026-02-26 01:01:41 -03:00
Sofia Rodrigues
5cb510cdf7 fix: precedence 2026-02-26 01:01:24 -03:00
Sofia Rodrigues
a72de461cd Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-25 22:34:45 -03:00
Sofia Rodrigues
228f0d24a7 fix: remove unused headers 2026-02-25 22:34:41 -03:00
Sofia Rodrigues
73cf41d7e5 fix: comments 2026-02-25 22:30:43 -03:00
Sofia Rodrigues
819d4c6c1f fix: uri comments 2026-02-25 22:13:19 -03:00
Sofia Rodrigues
4de3e40349 fix: builder 2026-02-25 22:00:11 -03:00
Sofia Rodrigues
03f1d47462 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-25 21:59:08 -03:00
Sofia Rodrigues
a88908572c Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-25 21:54:21 -03:00
Sofia Rodrigues
55d357dbb4 fix: dots 2026-02-25 21:54:18 -03:00
Sofia Rodrigues
49d00ae056 fix: comments and small formatting errors 2026-02-25 21:52:12 -03:00
Sofia Rodrigues
e9eed5cbe4 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-25 20:57:02 -03:00
Sofia Rodrigues
2652ae0fb8 fix: small changes 2026-02-25 20:56:19 -03:00
Sofia Rodrigues
3f48ef4af9 fix: comment 2026-02-25 20:45:57 -03:00
Sofia Rodrigues
a9de308aea fix: RFC checks and small improvements 2026-02-25 20:45:22 -03:00
Sofia Rodrigues
21821ef062 fix: encoding duplication 2026-02-24 14:28:14 -03:00
Sofia Rodrigues
5ba3a6d4fc Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-24 10:47:43 -03:00
Sofia Rodrigues
8492e58a82 fix: comments 2026-02-24 10:38:11 -03:00
Sofia Rodrigues
e65e20e1cb feat: field content 2026-02-24 09:57:22 -03:00
Sofia Rodrigues
de7c029c9f feat: field content 2026-02-24 09:56:52 -03:00
Sofia Rodrigues
89c992a3c9 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-24 09:26:59 -03:00
Sofia Rodrigues
0b76c3de69 fix: tests 2026-02-24 09:20:52 -03:00
Sofia Rodrigues
ff99979855 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-24 09:09:39 -03:00
Sofia Rodrigues
9ddbb59fe1 fix: format 2026-02-24 09:09:35 -03:00
Sofia Rodrigues
36f87f98f8 fix: char 2026-02-24 09:09:14 -03:00
Sofia Rodrigues
5914fe3a4a Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-24 08:54:43 -03:00
Sofia Rodrigues
29f651a89c fix: extension values 2026-02-24 08:53:16 -03:00
Sofia Rodrigues
2e1bdd922e 2026-02-24 08:35:36 -03:00
Sofia Rodrigues
ab5d50cbc3 fix: data char 2026-02-23 22:18:15 -03:00
Sofia Rodrigues
7902db17c2 fix: comments 2026-02-21 01:25:05 -03:00
Sofia Rodrigues
5626ee369c feat: specialize quote 2026-02-21 00:53:15 -03:00
Sofia Rodrigues
0fb57a405f fix: wrong comment 2026-02-20 18:02:46 -03:00
Sofia Rodrigues
bfa18ef30c fix: uri builder 2026-02-20 17:11:05 -03:00
Sofia Rodrigues
e0efb8aec9 fix: scheme parser 2026-02-20 16:59:24 -03:00
Sofia Rodrigues
530f6865f9 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-20 16:40:06 -03:00
Sofia Rodrigues
f97d86cf4b feat: trim headers 2026-02-20 16:40:00 -03:00
Sofia Rodrigues
781b9f561e Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-20 16:30:28 -03:00
Sofia Rodrigues
a9ac33d994 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-20 16:24:06 -03:00
Sofia Rodrigues
c457a98d6a fix: test 2026-02-20 16:23:24 -03:00
Sofia Rodrigues
8d8439bf0b fix: status code 2026-02-20 16:19:55 -03:00
Sofia Rodrigues
eddb5e139d Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-20 16:10:13 -03:00
Sofia Rodrigues
5a53207723 feat: remove direct access 2026-02-20 16:10:06 -03:00
Sofia Rodrigues
0d3f6e5481 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-20 16:09:16 -03:00
Sofia Rodrigues
96a017262c feat: reason phrase in custom status code 2026-02-20 16:08:26 -03:00
Sofia Rodrigues
462e3d02dd fix: import coe 2026-02-20 12:01:21 -03:00
Sofia Rodrigues
b38f01ef51 feat: ignore reasonphrase 2026-02-19 14:01:21 -03:00
Sofia Rodrigues
73bf2b5e04 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-19 13:36:29 -03:00
Sofia Rodrigues
c8c92fcf92 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-19 13:13:28 -03:00
Sofia Rodrigues
cf6b159da5 feat: validation in reasonPhrase 2026-02-19 13:13:22 -03:00
Sofia Rodrigues
319214cfb3 feat: config URI 2026-02-19 10:51:32 -03:00
Sofia Rodrigues
37c7b1e22c feat: reuse char 2026-02-18 11:03:35 -03:00
Sofia Rodrigues
42cfda23f3 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-18 10:56:23 -03:00
Sofia Rodrigues
78316b9ade fix: isToken 2026-02-18 10:55:55 -03:00
Sofia Rodrigues
dd09289d2b fix: duplication 2026-02-18 10:36:17 -03:00
Sofia Rodrigues
10a66e9f9a fix: uri duplication checkers 2026-02-18 10:35:04 -03:00
Sofia Rodrigues
ad4719399d fix: trailing peridos 2026-02-18 10:11:47 -03:00
Sofia Rodrigues
53fb1a25b3 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-18 09:45:02 -03:00
Sofia Rodrigues
3fdaf2df0c Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-18 09:44:01 -03:00
Sofia Rodrigues
4ba722f51c fix: reasonPhrase 2026-02-18 09:40:25 -03:00
Sofia Rodrigues
37ec94e2f0 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-17 21:08:30 -03:00
Sofia Rodrigues
157e3b032d Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-17 21:08:21 -03:00
Sofia Rodrigues
910c71954e feat: reason phrase 2026-02-17 21:07:48 -03:00
Sofia Rodrigues
9dd5634759 Merge branch 'sofia/async-http-headers' into sofia/async-http-uri 2026-02-17 11:47:57 -03:00
Sofia Rodrigues
a521ba3abd fix: typos and connection token 2026-02-17 11:47:46 -03:00
Sofia Rodrigues
6b0f05d075 Merge branch 'sofia/async-http-data' into sofia/async-http-headers 2026-02-17 11:12:22 -03:00
Sofia Rodrigues
61d6c02ecd fix: typos and compareName 2026-02-17 11:12:13 -03:00
Sofia Rodrigues
1c564ed5f7 fix: comment 2026-02-13 11:56:44 -03:00
Sofia Rodrigues
c4737fb66a fix: parse ab-empty 2026-02-13 11:52:53 -03:00
Sofia Rodrigues
43d3b2df91 merge: 'sofia/async-http-headers' 2026-02-13 11:43:55 -03:00
Sofia Rodrigues
87c5488c20 merge: 'sofia/async-http-headers' 2026-02-13 11:39:09 -03:00
Sofia Rodrigues
e0d5596e63 fix: typo 2026-02-13 11:37:08 -03:00
Sofia Rodrigues
1f2671db3d merge: branch 'sofia/async-http-data' 2026-02-13 11:35:33 -03:00
Sofia Rodrigues
940ab9bdb5 fix: typos 2026-02-13 11:33:18 -03:00
Sofia Rodrigues
b042b8efbd fix: parser path 2026-02-13 10:13:00 -03:00
Sofia Rodrigues
8c00ba48ae fix: parser 2026-02-13 10:12:22 -03:00
Sofia Rodrigues
d07f5c502f feat: specialize encodedstrings 2026-02-13 10:10:34 -03:00
Sofia Rodrigues
10337c620b fix: test 2026-02-13 01:57:23 -03:00
Sofia Rodrigues
692c7c1a09 fix: test 2026-02-13 01:56:29 -03:00
Sofia Rodrigues
cacfe00c1d fix: test 2026-02-13 01:54:52 -03:00
Sofia Rodrigues
451c11d5a1 fix: make strict 2026-02-13 01:52:04 -03:00
Sofia Rodrigues
e92fcf6d46 Merge branch 'sofia/async-http-headers' of https://github.com/leanprover/lean4 into sofia/async-http-uri 2026-02-13 01:41:20 -03:00
Sofia Rodrigues
2cc32928a4 feat: add parser features to path 2026-02-13 01:39:12 -03:00
Sofia Rodrigues
153513d5e2 fix: typos 2026-02-13 01:29:12 -03:00
Sofia Rodrigues
94308408a9 Merge branch 'sofia/async-http-data' of https://github.com/leanprover/lean4 into sofia/async-http-headers 2026-02-13 01:20:31 -03:00
Sofia Rodrigues
1ae6970b77 fix: comment 2026-02-13 01:19:51 -03:00
Sofia Rodrigues
9ce1821be0 feat: add trailers some type of headers 2026-02-12 12:46:15 -03:00
Sofia Rodrigues
eeff4847fe Merge branch 'sofia/async-http-data' of https://github.com/leanprover/lean4 into sofia/async-http-headers 2026-02-12 12:31:30 -03:00
Sofia Rodrigues
2956f88050 refactor: move trailers 2026-02-12 12:30:59 -03:00
Sofia Rodrigues
26d9c1c07b feat: add extension handling of quotes and ExtensionName 2026-02-12 12:21:47 -03:00
Sofia Rodrigues
0c44b4ae05 Merge branch 'sofia/async-http-headers' of https://github.com/leanprover/lean4 into sofia/async-http-uri 2026-02-11 19:29:54 -03:00
Sofia Rodrigues
3568464ca7 Merge branch 'sofia/async-http-data' of https://github.com/leanprover/lean4 into sofia/async-http-headers 2026-02-11 19:23:59 -03:00
Sofia Rodrigues
8e5296c71a fix: chunked 2026-02-11 19:22:30 -03:00
Sofia Rodrigues
2c23680163 fix: imports 2026-02-10 17:23:14 -03:00
Sofia Rodrigues
c4f179daa0 Merge branch 'sofia/async-http-headers' of https://github.com/leanprover/lean4 into sofia/async-http-uri 2026-02-10 17:15:31 -03:00
Sofia Rodrigues
c2f657a15a Merge branch 'sofia/async-http-data' of https://github.com/leanprover/lean4 into sofia/async-http-headers 2026-02-10 17:12:45 -03:00
Sofia Rodrigues
9332081875 fix: import 2026-02-10 17:12:20 -03:00
Sofia Rodrigues
1cec97568b fix: imports 2026-02-10 17:11:11 -03:00
Sofia Rodrigues
b567713641 Merge branch 'sofia/async-http-data' of https://github.com/leanprover/lean4 into sofia/async-http-headers 2026-02-10 17:03:44 -03:00
Sofia Rodrigues
de776c1f32 fix: interpolation 2026-02-10 17:03:02 -03:00
Sofia Rodrigues
c498ea74ec Merge branch 'sofia/async-http-headers' of https://github.com/leanprover/lean4 into sofia/async-http-uri 2026-02-10 16:47:08 -03:00
Sofia Rodrigues
f4aad3a494 Merge branch 'sofia/async-http-data' of https://github.com/leanprover/lean4 into sofia/async-http-headers 2026-02-10 16:46:48 -03:00
Sofia Rodrigues
1cebf576c3 Merge branch 'master' of https://github.com/leanprover/lean4 into sofia/async-http-data 2026-02-10 16:44:20 -03:00
Sofia Rodrigues
03843fd3f0 fix: suggestions 2026-02-09 21:09:38 -03:00
Sofia Rodrigues
63984c8dda fix: header value 2026-02-09 12:01:25 -03:00
Sofia Rodrigues
e2fd8a5835 Merge branch 'sofia/async-http-data' of https://github.com/leanprover/lean4 into sofia/async-http-headers 2026-02-09 11:28:52 -03:00
Sofia Rodrigues
a0263870b9 fix: extensions 2026-02-09 11:24:48 -03:00
Sofia Rodrigues
3c4ae58aff feat: add extensions 2026-02-09 11:18:24 -03:00
Sofia Rodrigues
5965707575 fix: apply suggestions 2026-02-09 11:17:03 -03:00
Sofia Rodrigues
dbe0140578 fix: enforce validations 2026-02-09 10:28:43 -03:00
Sofia Rodrigues
bad70e3eab feat: request type has request target 2026-01-25 12:36:36 -03:00
Sofia Rodrigues
21286eb163 fix: domain name comment 2026-01-25 12:36:36 -03:00
Sofia Rodrigues
0e5f07558c feat: introduce data type for HTTP 2026-01-25 12:36:36 -03:00
Sofia Rodrigues
6e26b901e4 fix: encoding 2026-01-25 12:33:07 -03:00
Sofia Rodrigues
81c67c8f12 revert: levenshtein test 2026-01-25 12:29:37 -03:00
Sofia Rodrigues
990e21eefc fix: namespace 2026-01-25 12:27:16 -03:00
Sofia Rodrigues
7141144a2f fix: remove native_decide 2026-01-25 12:27:16 -03:00
Sofia Rodrigues
8c343501c1 fix: apply suggestions
Co-authored-by: Rob23oba <152706811+Rob23oba@users.noreply.github.com>
2026-01-25 12:27:16 -03:00
Sofia Rodrigues
44f08686cd feat: connection values 2026-01-25 12:27:16 -03:00
Sofia Rodrigues
65883f8c2a fix: levenshtein test is using the new Decidable instance 2026-01-25 12:27:16 -03:00
Sofia Rodrigues
bd28a8fad5 fix: tests and type class 2026-01-25 12:27:16 -03:00
Sofia Rodrigues
8ba86c2c67 fix: case and usage of native_decide 2026-01-25 12:24:52 -03:00
Sofia Rodrigues
d3cddf9e44 fix: Headers.Basic comment 2026-01-25 12:24:52 -03:00
Sofia Rodrigues
5f3babee5c feat: headers data structure 2026-01-25 12:24:51 -03:00
Sofia Rodrigues
26dfc9a872 refactor: remove headers 2026-01-25 12:16:13 -03:00
Sofia Rodrigues
e47439e8be fix: default size 2026-01-25 11:26:03 -03:00
Sofia Rodrigues
1ef53758be fix: apply suggestions
Co-authored-by: Rob23oba <152706811+Rob23oba@users.noreply.github.com>
2026-01-25 11:21:06 -03:00
Sofia Rodrigues
8544042789 fix: status and chunkedbuffer 2026-01-25 11:19:34 -03:00
Sofia Rodrigues
f564d43d98 feat: basic headers structure to more structured approach 2026-01-23 17:58:44 -03:00
Sofia Rodrigues
32fa0666c9 feat: data components 2026-01-23 17:14:53 -03:00
10 changed files with 3189 additions and 52 deletions

View File

@@ -13,6 +13,7 @@ public import Std.Internal.Http.Data.Response
public import Std.Internal.Http.Data.Status
public import Std.Internal.Http.Data.Chunk
public import Std.Internal.Http.Data.Headers
public import Std.Internal.Http.Data.URI
/-!
# HTTP Data Types

View File

@@ -10,6 +10,7 @@ public import Std.Internal.Http.Data.Extensions
public import Std.Internal.Http.Data.Method
public import Std.Internal.Http.Data.Version
public import Std.Internal.Http.Data.Headers
public import Std.Internal.Http.Data.URI
public section
@@ -50,7 +51,7 @@ structure Request.Head where
/--
The raw request-target string (commonly origin-form path/query, `"*"`, or authority-form).
-/
uri : String
uri : RequestTarget := .asteriskForm
/--
Collection of HTTP headers for the request (Content-Type, Authorization, etc.).
@@ -85,7 +86,7 @@ structure Request.Builder where
/--
The request-line of an HTTP request.
-/
line : Head := { method := .get, version := .v11, uri := "*" }
line : Head := { method := .get, version := .v11, uri := .asteriskForm }
/--
Optional dynamic metadata attached to the request.
@@ -108,7 +109,7 @@ instance : Encode .v11 Head where
encode buffer req :=
let buffer := Encode.encode (v := .v11) buffer req.method
let buffer := buffer.writeChar ' '
let buffer := buffer.writeString req.uri
let buffer := Encode.encode (v := .v11) buffer req.uri
let buffer := buffer.writeChar ' '
let buffer := Encode.encode (v := .v11) buffer req.version
let buffer := buffer.writeString "\r\n"
@@ -144,11 +145,18 @@ def version (builder : Builder) (version : Version) : Builder :=
/--
Sets the request target/URI for the request being built.
-/
def uri (builder : Builder) (uri : String) : Builder :=
def uri (builder : Builder) (uri : RequestTarget) : Builder :=
{ builder with line := { builder.line with uri := uri } }
/--
Sets the headers for the request being built.
Sets the request target/URI for the request being built
-/
def uri! (builder : Builder) (uri : String) : Builder :=
let uri := RequestTarget.parse! uri
{ builder with line := { builder.line with uri } }
/--
Sets the headers for the request being built
-/
def headers (builder : Builder) (headers : Headers) : Builder :=
{ builder with line := { builder.line with headers } }
@@ -201,7 +209,7 @@ end Builder
/--
Creates a new HTTP GET Request with the specified URI.
-/
def get (uri : String) : Builder :=
def get (uri : RequestTarget) : Builder :=
new
|>.method .get
|>.uri uri
@@ -209,7 +217,7 @@ def get (uri : String) : Builder :=
/--
Creates a new HTTP POST Request builder with the specified URI.
-/
def post (uri : String) : Builder :=
def post (uri : RequestTarget) : Builder :=
new
|>.method .post
|>.uri uri
@@ -217,7 +225,7 @@ def post (uri : String) : Builder :=
/--
Creates a new HTTP PUT Request builder with the specified URI.
-/
def put (uri : String) : Builder :=
def put (uri : RequestTarget) : Builder :=
new
|>.method .put
|>.uri uri
@@ -225,7 +233,7 @@ def put (uri : String) : Builder :=
/--
Creates a new HTTP DELETE Request builder with the specified URI.
-/
def delete (uri : String) : Builder :=
def delete (uri : RequestTarget) : Builder :=
new
|>.method .delete
|>.uri uri
@@ -233,7 +241,7 @@ def delete (uri : String) : Builder :=
/--
Creates a new HTTP PATCH Request builder with the specified URI.
-/
def patch (uri : String) : Builder :=
def patch (uri : RequestTarget) : Builder :=
new
|>.method .patch
|>.uri uri
@@ -241,25 +249,25 @@ def patch (uri : String) : Builder :=
/--
Creates a new HTTP HEAD Request builder with the specified URI.
-/
def head (uri : String) : Builder :=
def head (uri : RequestTarget) : Builder :=
new
|>.method .head
|>.uri uri
/--
Creates a new HTTP OPTIONS Request builder with the specified URI.
Use `Request.options "*"` for server-wide OPTIONS.
Use `Request.options (RequestTarget.asteriskForm)` for server-wide OPTIONS.
-/
def options (uri : String) : Builder :=
def options (uri : RequestTarget) : Builder :=
new
|>.method .options
|>.uri uri
/--
Creates a new HTTP CONNECT Request builder with the specified URI.
Typically used with authority-form URIs such as `"example.com:443"` for tunneling.
Typically used with `RequestTarget.authorityForm` for tunneling.
-/
def connect (uri : String) : Builder :=
def connect (uri : RequestTarget) : Builder :=
new
|>.method .connect
|>.uri uri
@@ -267,7 +275,7 @@ def connect (uri : String) : Builder :=
/--
Creates a new HTTP TRACE Request builder with the specified URI.
-/
def trace (uri : String) : Builder :=
def trace (uri : RequestTarget) : Builder :=
new
|>.method .trace
|>.uri uri

View File

@@ -0,0 +1,97 @@
/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sofia Rodrigues
-/
module
prelude
public import Std.Internal.Http.Data.URI.Basic
public import Std.Internal.Http.Data.URI.Parser
public section
/-!
# URI
This module defines the `URI` and `RequestTarget` types that represent and manipulate components of
URIs as defined by RFC 3986. It provides parsing, rendering, and normalization utilities for working
with URIs and request targets in HTTP messages.
References:
* https://www.rfc-editor.org/rfc/rfc3986.html
* https://www.rfc-editor.org/rfc/rfc9112.html#section-3.3
-/
namespace Std.Http.RequestTarget
set_option linter.all true
/--
Attempts to parse a `RequestTarget` from the given string.
-/
@[inline]
def parse? (string : String) : Option RequestTarget :=
(URI.Parser.parseRequestTarget <* Std.Internal.Parsec.eof).run string.toUTF8 |>.toOption
/--
Parses a `RequestTarget` from the given string. Panics if parsing fails. Use `parse?`
if you need a safe option-returning version.
-/
@[inline]
def parse! (string : String) : RequestTarget :=
match parse? string with
| some res => res
| none => panic! "invalid request target"
/--
Creates an origin-form request target from a path string.
The path should start with '/' (e.g., "/api/users" or "/search?q=test").
Panics if the string is not a valid origin-form request target.
-/
@[inline]
def originForm! (path : String) : RequestTarget :=
match parse? path with
| some (.originForm p q) => .originForm p q
| _ => panic! s!"invalid origin-form request target: {path}"
end RequestTarget
namespace URI
/--
Attempts to parse a `URI` from the given string.
-/
@[inline]
def parse? (string : String) : Option URI :=
(Parser.parseURI <* Std.Internal.Parsec.eof).run string.toUTF8 |>.toOption
/--
Parses a `URI` from the given string. Panics if parsing fails. Use `parse?` if you need a safe
option-returning version.
-/
@[inline]
def parse! (string : String) : URI :=
match parse? string with
| some res => res
| none => panic! "invalid URI"
namespace Path
/--
Attempts to parse a URI path from the given string.
Returns `none` if the string is not a valid path.
-/
@[inline]
def parse? (s : String) : Option Std.Http.URI.Path :=
(Std.Http.URI.Parser.parsePath {} true true <* Std.Internal.Parsec.eof).run s.toUTF8 |>.toOption
/--
Parses a URI path from the given string. Returns the root path `"/"` if parsing fails.
-/
@[inline]
def parseOrRoot (s : String) : Std.Http.URI.Path :=
parse? s |>.getD { segments := #[], absolute := true }
end Std.Http.URI.Path

View File

@@ -0,0 +1,916 @@
/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sofia Rodrigues
-/
module
prelude
import Init.Data.ToString
public import Std.Net
public import Std.Internal.Http.Internal
public import Std.Internal.Http.Data.URI.Encoding
public section
/-!
# URI Structure
This module defines an HTTP-oriented URI structure aligned with RFC 3986 and RFC 9110, including
schemes, authorities, paths, queries, fragments, and request targets.
Host handling is intentionally strict: this module accepts IPv4, bracketed IPv6, and DNS-style
domain names (LDH labels). RFC 3986 `reg-name` forms that are not DNS-compatible are rejected.
All text components use the encoding types from `Std.Http.URI.Encoding` to ensure proper
percent-encoding is maintained throughout.
References:
* https://www.rfc-editor.org/rfc/rfc3986.html
* https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-references
-/
namespace Std.Http
set_option linter.all true
open Internal Char
namespace URI
/--
Proposition that `s` is a valid URI scheme per RFC 3986:
`scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )`.
The scheme value stored in this module is normalized to lowercase.
Reference: https://www.rfc-editor.org/rfc/rfc3986.html#section-3.1
-/
abbrev IsValidScheme (s : String) : Prop :=
IsLowerCase s
s.toList.all isValidSchemeChar
(s.toList.head?.map isAlpha |>.getD false) -- first character must be ALPHA
/--
URI scheme identifier (e.g., "http", "https", "ftp").
-/
abbrev Scheme := { s : String // IsValidScheme s }
instance : Inhabited Scheme where
default := "http", by decide, by decide, by decide
namespace Scheme
/--
Attempts to create a `Scheme` from a string, normalizing to lowercase.
Returns `none` if the scheme is invalid per RFC 3986 Section 3.1.
-/
def ofString? (s : String) : Option Scheme :=
let lower := s.toLower
if h : IsValidScheme lower then
some lower, h
else
none
/--
Creates a `Scheme` from a string, normalizing to lowercase. Panics if invalid.
-/
def ofString! (s : String) : Scheme :=
match ofString? s with
| some scheme => scheme
| none => panic! s!"invalid URI scheme: {s.quote}"
/--
Returns the default port number for this URI scheme: 443 for `https`, 80 for everything else.
-/
def defaultPort (scheme : URI.Scheme) : UInt16 :=
if scheme.val == "https" then 443 else 80
/--
Returns the URI scheme for a given port: `"https"` for 443, `"http"` otherwise.
-/
def ofPort (port : UInt16) : URI.Scheme :=
if port == 443 then "https", by decide else "http", by decide
end Scheme
/--
User information component containing an encoded username and optional encoded password.
The stored strings use URI userinfo percent-encoding so parsed URIs can be rendered without
losing percent-encoding choices (for example, `%3A` versus `:`).
Note: embedding passwords in URIs is deprecated per RFC 9110 Section 4.2.4. Avoid using the
password field in new code, and never include it in logs or error messages.
Reference: https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.1
-/
structure UserInfo where
/--
The encoded username.
-/
username : EncodedUserInfo
/--
The optional encoded password.
-/
password : Option EncodedUserInfo
deriving Inhabited, Repr, BEq
namespace UserInfo
/--
Builds a `UserInfo` value from raw strings by applying userinfo percent-encoding.
-/
@[inline]
def ofStrings (username : String) (password : Option String := none) : UserInfo where
username := EncodedUserInfo.encode username
password := EncodedUserInfo.encode <$> password
/--
Returns the decoded username, or `none` if decoding fails UTF-8 validation.
-/
@[inline]
def username? (ui : UserInfo) : Option String :=
ui.username.decode
/--
Returns the decoded password when present, or `none` if absent or decoding fails UTF-8 validation.
-/
@[inline]
def password? (ui : UserInfo) : Option String :=
ui.password.bind EncodedUserInfo.decode
end UserInfo
/--
Checks whether a single domain label is valid. A label must be non-empty, contain only ASCII
alphanumeric characters and `-`, cannot start or end with `-`, and must be at most 63 characters.
References:
* https://www.rfc-editor.org/rfc/rfc1034.html#section-3.5
* https://www.rfc-editor.org/rfc/rfc1123.html#section-2.1
-/
def isValidDomainLabel (s : String) : Bool :=
let chars := s.toList
decide (chars.length 63) &&
chars.all (fun c => isAsciiAlphaNumChar c c = '-') &&
(chars.head?.map isAsciiAlphaNumChar |>.getD false) &&
(chars.getLast?.map isAsciiAlphaNumChar |>.getD false)
/--
Proposition that asserts `s` is a valid dot-separated domain name.
Each label must satisfy `IsValidDomainLabel`, and the full name must be at most 255 characters.
-/
abbrev IsValidDomainName (s : String) : Prop :=
let labels := s.splitOn "."
¬labels.isEmpty labels.all isValidDomainLabel s.length 255
/--
A domain name represented as a validated, lowercase-normalized string.
Only ASCII alphanumeric characters, hyphens, and dots are allowed.
Each label cannot start or end with `-` and is limited to 63 characters.
Internationalized domain names must be converted to punycode before use.
Reference: https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.2
-/
abbrev DomainName := { s : String // IsLowerCase s IsValidDomainName s ¬s.isEmpty }
namespace DomainName
/--
Attempts to create a normalized domain name from a string.
Returns `none` if the name is empty, longer than 255 characters, or any label violates DNS label
constraints.
-/
def ofString? (s : String) : Option DomainName :=
let lower := s.toLower
if h₁ : lower.isEmpty then
none
else if h₃ : IsValidDomainName lower then
some lower, IsLowerCase.isLowerCase_toLower, h₃, h₁
else
none
end DomainName
/--
Host component of a URI, supporting domain names and IP addresses.
Reference: https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.2
-/
inductive Host
/--
A domain name (lowercase-normalized).
-/
| name (name : DomainName)
/--
An IPv4 address.
-/
| ipv4 (ipv4 : Net.IPv4Addr)
/--
An IPv6 address.
-/
| ipv6 (ipv6 : Net.IPv6Addr)
deriving Inhabited, BEq
instance : Repr Host where
reprPrec x prec :=
let nestPrec := (if prec 1024 then 1 else 2)
let name := "Std.Http.URI.Host"
let repr (ctr : String) a :=
Repr.addAppParen (Format.nest nestPrec (.text s!"{name}.{ctr}" ++ .line ++ a)).group prec
match x with
| Host.name a => repr "name" (reprArg a)
| Host.ipv4 a => repr "ipv4" (toString a)
| Host.ipv6 a => repr "ipv6" (toString a)
instance : ToString Host where
toString
| .name n => n
| .ipv4 addr => toString addr
| .ipv6 addr => s!"[{toString addr}]"
/--
Authority port representation, preserving the distinction between:
* no port separator (`example.com`)
* empty port (`example.com:`)
* numeric port (`example.com:443`)
-/
inductive Port where
/--
No `:` port separator is present (for example, `example.com`).
-/
| omitted
/--
A `:` port separator is present with no digits after it (for example, `example.com:`).
-/
| empty
/--
A numeric port value is present (for example, `example.com:443`).
-/
| value (port : UInt16)
deriving Inhabited, Repr, DecidableEq
/--
The authority component of a URI, identifying the network location of the resource.
Reference: https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2
-/
structure Authority where
/--
Optional user information (username and password).
-/
userInfo : Option UserInfo := none
/--
The host identifying the network location.
-/
host : Host
/--
Port component, preserving whether it is omitted (`example.com`),
explicitly empty (`example.com:`), or numeric (`example.com:443`).
-/
port : Port := .omitted
deriving Inhabited, Repr, BEq
instance : ToString Authority where
toString auth :=
let userPart := match auth.userInfo with
| none => ""
| some name, some pass => s!"{name}:{pass}@"
| some name, none => s!"{name}@"
let hostPart := toString auth.host
let portPart := match auth.port with
| .omitted => ""
| .empty => ":"
| .value p => s!":{p}"
s!"{userPart}{hostPart}{portPart}"
/--
Hierarchical path component of a URI. Each segment is stored as an `EncodedSegment` to maintain
proper percent-encoding.
Reference: https://www.rfc-editor.org/rfc/rfc3986.html#section-3.3
-/
structure Path where
/--
The path segments making up the hierarchical structure (each segment is percent-encoded).
-/
segments : Array EncodedSegment
/--
Whether the path is absolute (begins with '/') or relative.
-/
absolute : Bool
deriving Inhabited, Repr, BEq
instance : ToString Path where
toString path :=
let result := String.intercalate "/" (path.segments.map toString).toList
if path.absolute then "/" ++ result else result
namespace Path
/--
Returns true if the path has no segments.
-/
def isEmpty (p : Path) : Bool := p.segments.isEmpty
/--
Returns the parent path by removing the last segment. If the path is empty, returns the path unchanged.
-/
def parent (p : Path) : Path :=
if p.segments.isEmpty then p
else { p with segments := p.segments.pop }
/--
Joins two paths. If the second path is absolute, it is returned as-is. Otherwise, the second path's
segments are appended to the first path.
-/
def join (p1 : Path) (p2 : Path) : Path :=
if p2.absolute then p2
else { p1 with segments := p1.segments ++ p2.segments }
/--
Appends a single segment to the path. The segment will be percent-encoded.
-/
def append (p : Path) (segment : String) : Path :=
{ p with segments := p.segments.push (EncodedSegment.encode segment) }
/--
Appends an already-encoded segment to the path.
-/
def appendEncoded (p : Path) (segment : EncodedSegment) : Path :=
{ p with segments := p.segments.push segment }
/--
Removes dot segments from the path according to RFC 3986 Section 5.2.4. This handles "."
(current directory) and ".." (parent directory) segments.
-/
def normalize (p : Path) : Path :=
let rec loop (input : List (EncodedSegment)) (output : List (EncodedSegment)) : List (EncodedSegment) :=
match input with
| [] =>
output.reverse
| segStr :: rest =>
if toString segStr == "." then
loop rest output
else if toString segStr == ".." then
match output with
| [] => loop rest []
| _ :: tail => loop rest tail
else
loop rest (segStr :: output)
{ p with segments := (loop p.segments.toList []).toArray }
/--
Returns the path segments as decoded strings.
Segments that cannot be decoded as UTF-8 are returned as their raw encoded form.
-/
def toDecodedSegments (p : Path) : Array String :=
p.segments.map fun seg =>
seg.decode.getD (toString seg)
end Path
/--
Query string represented as an array of key-value pairs. Both keys and values are stored as
`EncodedQueryParam` for proper application/x-www-form-urlencoded encoding. Values are optional to
support parameters without values (e.g., "?flag"). Order is preserved based on insertion order.
Reference: https://www.rfc-editor.org/rfc/rfc3986.html#section-3.4
-/
@[expose]
def Query := Array (EncodedQueryParam × Option EncodedQueryParam)
deriving Repr, Inhabited, BEq
namespace Query
/--
Extracts all unique query parameter names.
-/
@[expose]
def names (query : Query) : Array EncodedQueryParam :=
query.map (fun p => p.fst)
|> Array.toList
|> List.eraseDups
|> List.toArray
/--
Extracts all query parameter values.
-/
@[expose]
def values (query : Query) : Array (Option EncodedQueryParam) :=
query.map (fun p => p.snd)
/--
Returns the query as an array of (key, value) pairs. This is an identity function since Query is
already an array of pairs.
-/
@[expose]
def toArray (query : Query) : Array (EncodedQueryParam × Option EncodedQueryParam) :=
query
/--
Formats a query parameter as a string in the format "key" or "key=value". The key and value are
already percent-encoded as `EncodedQueryParam`.
-/
def formatQueryParam (key : EncodedQueryParam) (value : Option EncodedQueryParam) : String :=
match value with
| none => toString key
| some v => s!"{toString key}={toString v}"
/--
Finds the first value of a query parameter by key name. Returns `none` if the key is not found.
The value remains encoded as `EncodedQueryParam`.
-/
def findEncoded? (query : Query) (key : EncodedQueryParam) : Option (Option EncodedQueryParam) :=
let matchingKey := Array.find? (fun x => x.fst.toByteArray = key.toByteArray) query
matchingKey.map (fun x => x.snd)
/--
Finds the first value of a query parameter by raw key string. The key is percent-encoded before
matching. This avoids aliasing between raw and pre-encoded spellings.
-/
def find? (query : Query) (key : String) : Option (Option EncodedQueryParam) :=
query.findEncoded? (EncodedQueryParam.encode key)
/--
Finds all values of a query parameter by key name. Returns an empty array if the key is not found.
The values remain encoded as `EncodedQueryParam`.
-/
def findAllEncoded (query : Query) (key : EncodedQueryParam) : Array (Option EncodedQueryParam) :=
query.filterMap (fun x =>
if x.fst.toByteArray = key.toByteArray then
some x.snd
else
none)
/--
Finds all values of a query parameter by raw key string. The key is percent-encoded before matching.
-/
def findAll (query : Query) (key : String) : Array (Option EncodedQueryParam) :=
query.findAllEncoded (EncodedQueryParam.encode key)
/--
Adds a query parameter to the query string.
-/
def insert (query : Query) (key : String) (value : String) : Query :=
let encodedKey : EncodedQueryParam := EncodedQueryParam.encode key
let encodedValue : EncodedQueryParam := EncodedQueryParam.encode value
query.push (encodedKey, some encodedValue)
/--
Adds an already-encoded key-value pair to the query string.
-/
def insertEncoded (query : Query) (key : EncodedQueryParam) (value : Option EncodedQueryParam) : Query :=
query.push (key, value)
/--
Creates an empty query string.
-/
def empty : Query := #[]
/--
Creates a query string from a list of key-value pairs.
-/
def ofList (pairs : List (EncodedQueryParam × Option EncodedQueryParam)) : Query :=
pairs.toArray
/--
Checks if a query parameter exists.
-/
def containsEncoded (query : Query) (key : EncodedQueryParam) : Bool :=
query.any (fun x => x.fst.toByteArray = key.toByteArray)
/--
Checks if a query parameter exists by raw key string. The key is percent-encoded before matching.
-/
def contains (query : Query) (key : String) : Bool :=
query.containsEncoded (EncodedQueryParam.encode key)
/--
Removes all occurrences of a query parameter by key name.
-/
def eraseEncoded (query : Query) (key : EncodedQueryParam) : Query :=
query.filter (fun x =>
x.fst.toByteArray key.toByteArray
)
/--
Removes all occurrences of a query parameter by raw key string. The key is percent-encoded before matching.
-/
def erase (query : Query) (key : String) : Query :=
query.eraseEncoded (EncodedQueryParam.encode key)
/--
Gets the first value of a query parameter by key name, decoded as a string.
Returns `none` if the key is not found or if the value cannot be decoded as UTF-8.
-/
def get (query : Query) (key : String) : Option String :=
match query.find? key with
| none => none
| some none => some "" -- Key exists but has no value
| some (some encoded) => encoded.decode
/--
Gets the first value of a query parameter by key name, decoded as a string.
Returns the default value if the key is not found or if the value cannot be decoded.
-/
def getD (query : Query) (key : String) (default : String) : String :=
query.get key |>.getD default
/--
Sets a query parameter, replacing all existing values for that key.
Both key and value will be automatically percent-encoded.
-/
def set (query : Query) (key : String) (value : String) : Query :=
query.erase key |>.insert key value
/--
Converts the query to a properly encoded query string format.
Example: "key1=value1&key2=value2&flag"
-/
def toRawString (query : Query) : String :=
let params := query.map (fun (k, v) => formatQueryParam k v)
String.intercalate "&" params.toList
instance : EmptyCollection Query :=
Query.empty
instance : Singleton (String × String) Query :=
fun k, v => Query.empty.insert k v
instance : Insert (String × String) Query :=
fun k, v q => q.insert k v
instance : ToString Query where
toString q :=
if q.isEmpty then "" else
let encodedParams := q.toList.map fun (key, value) =>
Query.formatQueryParam key value
"?" ++ String.intercalate "&" encodedParams
end Query
end URI
/--
Complete URI structure following RFC 3986. All text components use encoded string types to ensure
proper percent-encoding.
Reference: https://www.rfc-editor.org/rfc/rfc3986.html
-/
structure URI where
/--
The URI scheme (e.g., "http", "https", "ftp").
-/
scheme : URI.Scheme
/--
Optional authority component (user info, host, and port).
-/
authority : Option URI.Authority
/--
The hierarchical path component.
-/
path : URI.Path
/--
Optional query string as key-value pairs.
-/
query : URI.Query
/--
Optional fragment identifier (the part after '#'), percent-encoded.
-/
fragment : Option String
deriving Repr, Inhabited, BEq
instance : ToString URI where
toString uri :=
let schemePart := uri.scheme
let authorityPart := match uri.authority with
| none => ""
| some auth => s!"//{toString auth}"
let pathPart := toString uri.path
let queryPart := toString uri.query
let fragmentPart := uri.fragment.map (fun f => "#" ++ toString (URI.EncodedFragment.encode f)) |>.getD ""
s!"{schemePart}:{authorityPart}{pathPart}{queryPart}{fragmentPart}"
namespace URI
/--
Fluent builder for constructing URIs. Takes raw (unencoded) strings and handles encoding
automatically when building the final URI.
-/
structure Builder where
/--
The URI scheme (e.g., "http", "https").
-/
scheme : Option URI.Scheme := none
/--
User information (username and optional password).
-/
userInfo : Option UserInfo := none
/--
The host component.
-/
host : Option Host := none
/--
The port number.
-/
port : URI.Port := .omitted
/--
Path segments (will be encoded when building).
-/
pathSegments : Array String := #[]
/--
Query parameters as (key, optional value) pairs (will be encoded when building).
-/
query : Array (String × Option String) := #[]
/--
Fragment identifier (will be encoded when building).
-/
fragment : Option String := none
deriving Inhabited
namespace Builder
/--
Creates an empty URI builder.
-/
def empty : Builder := {}
/--
Attempts to set the URI scheme (e.g., "http", "https").
Returns `none` if the scheme is not a valid RFC 3986 scheme.
The stored scheme is normalized to lowercase.
-/
def setScheme? (b : Builder) (scheme : String) : Option Builder :=
URI.Scheme.ofString? scheme |>.map (fun scheme => { b with scheme := some scheme })
/--
Sets the URI scheme (e.g., "http", "https"). Panics if the scheme is invalid.
Use `setScheme?` if you need a safe option-returning version.
-/
def setScheme! (b : Builder) (scheme : String) : Builder :=
match b.setScheme? scheme with
| some b => b
| none => panic! s!"invalid URI scheme: {scheme.quote}"
/--
Sets the user information with username and optional password.
The strings will be automatically percent-encoded.
-/
def setUserInfo (b : Builder) (username : String) (password : Option String := none) : Builder :=
{ b with userInfo := some (UserInfo.ofStrings username password) }
/--
Sets the host as a domain name, returning `none` if the name contains invalid characters.
The domain name will be automatically lowercased.
Only ASCII alphanumeric characters, hyphens, and dots are allowed.
Each label cannot start or end with `-` and is limited to 63 characters.
Internationalized domain names must be converted to punycode before use.
-/
def setHost? (b : Builder) (name : String) : Option Builder :=
URI.DomainName.ofString? name |>.map (fun name => { b with host := some (Host.name name) })
/--
Sets the host as a domain name, panicking if the name contains invalid characters.
The domain name will be automatically lowercased.
Only ASCII alphanumeric characters, hyphens, and dots are allowed.
Each label cannot start or end with `-` and is limited to 63 characters.
Internationalized domain names must be converted to punycode before use.
-/
def setHost! (b : Builder) (name : String) : Builder :=
match b.setHost? name with
| some b => b
| none => panic! s!"invalid domain name: {name.quote}"
/--
Sets the host as an IPv4 address.
-/
def setHostIPv4 (b : Builder) (addr : Net.IPv4Addr) : Builder :=
{ b with host := some (Host.ipv4 addr) }
/--
Sets the host as an IPv6 address.
-/
def setHostIPv6 (b : Builder) (addr : Net.IPv6Addr) : Builder :=
{ b with host := some (Host.ipv6 addr) }
/--
Sets the port number.
-/
def setPort (b : Builder) (port : UInt16) : Builder :=
{ b with port := .value port }
/--
Replaces all path segments. Segments will be automatically percent-encoded when building.
-/
def setPath (b : Builder) (segments : Array String) : Builder :=
{ b with pathSegments := segments }
/--
Appends a single segment to the path. The segment will be automatically percent-encoded when building.
-/
def appendPathSegment (b : Builder) (segment : String) : Builder :=
{ b with pathSegments := b.pathSegments.push segment }
/--
Adds a query parameter with a value. Both key and value will be automatically percent-encoded when
building.
-/
def addQueryParam (b : Builder) (key : String) (value : String) : Builder :=
{ b with query := b.query.push (key, some value) }
/--
Adds a query parameter without a value (flag parameter). The key will be automatically
percent-encoded when building.
-/
def addQueryFlag (b : Builder) (key : String) : Builder :=
{ b with query := b.query.push (key, none) }
/--
Replaces all query parameters. Keys and values will be automatically percent-encoded when building.
-/
def setQuery (b : Builder) (query : Array (String × Option String)) : Builder :=
{ b with query := query }
/--
Sets the fragment identifier. The fragment will be automatically percent-encoded when building.
-/
def setFragment (b : Builder) (fragment : String) : Builder :=
{ b with fragment := some fragment }
/--
Builds a complete URI from the builder state, encoding all components. Defaults to "https" scheme if
none is specified.
-/
def build (b : Builder) : URI :=
let scheme := (b.scheme.getD "https", by decide)
let authority :=
if b.host.isSome then
some {
userInfo := b.userInfo
host := b.host.getD default
port := b.port
}
else none
let path : Path := {
segments := b.pathSegments.map EncodedSegment.encode
absolute := true
}
let query :=
b.query.map fun (k, v) =>
(EncodedQueryParam.encode k, v.map EncodedQueryParam.encode)
let query := URI.Query.ofList query.toList
{
scheme
authority := authority
path
query := query
fragment := b.fragment
}
end Builder
/--
Returns a new URI with the scheme replaced.
-/
def withScheme! (uri : URI) (scheme : String) : URI :=
{ uri with scheme := URI.Scheme.ofString! scheme }
/--
Returns a new URI with the authority replaced.
-/
def withAuthority (uri : URI) (authority : Option URI.Authority) : URI :=
{ uri with authority }
/--
Returns a new URI with the path replaced.
-/
def withPath (uri : URI) (path : URI.Path) : URI :=
{ uri with path }
/--
Returns a new URI with the query replaced.
-/
def withQuery (uri : URI) (query : URI.Query) : URI :=
{ uri with query }
/--
Returns a new URI with the fragment replaced.
-/
def withFragment (uri : URI) (fragment : Option String) : URI :=
{ uri with fragment }
/--
Partially normalizes a URI by removing dot-segments from the path (`.` and `..`)
according to RFC 3986 Section 5.2.4.
This does not apply the full normalization set from RFC 3986 Section 6 — for example,
case normalization, percent-encoding normalization, and default-port normalization are
not performed.
-/
def normalize (uri : URI) : URI :=
{ uri with path := uri.path.normalize }
end URI
/--
HTTP request target forms as defined in RFC 9112 Section 3.3.
Reference: https://www.rfc-editor.org/rfc/rfc9112.html#section-3.3
-/
inductive RequestTarget where
/--
Origin-form request target (most common for HTTP requests). Consists of a path and an optional query string.
Example: `/path/to/resource?key=value`
-/
| originForm (path : URI.Path) (query : Option URI.Query)
/--
Absolute-form request target containing a complete URI. Used when making requests through a proxy.
Example: `http://example.com:8080/path?key=value`
-/
| absoluteForm (uri : URI)
/--
Authority-form request target (used for CONNECT requests).
Example: `example.com:443`
-/
| authorityForm (authority : URI.Authority)
/--
Asterisk-form request target (used with OPTIONS requests).
Example: `*`
-/
| asteriskForm
deriving Inhabited, Repr
namespace RequestTarget
/--
Extracts the path component from a request target, if available.
Returns an empty relative path for targets without a path.
-/
def path : RequestTarget URI.Path
| .originForm p _ => p
| .absoluteForm uri => uri.path
| _ => { segments := #[], absolute := false }
/--
Extracts the query component from a request target, if available.
Returns an empty array for targets without a query.
-/
def query : RequestTarget URI.Query
| .originForm _ q => q.getD URI.Query.empty
| .absoluteForm uri => uri.query
| _ => URI.Query.empty
/--
Extracts the authority component from a request target, if available.
-/
def authority? : RequestTarget Option URI.Authority
| .authorityForm a => some a
| .absoluteForm uri => uri.authority
| _ => none
instance : ToString RequestTarget where
toString
| .originForm path query =>
let pathStr := toString path
let queryStr := query.map toString |>.getD ""
s!"{pathStr}{queryStr}"
| .absoluteForm uri => toString uri
| .authorityForm auth => toString auth
| .asteriskForm => "*"
instance : Encode .v11 RequestTarget where
encode buffer target := buffer.writeString (toString target)
end Std.Http.RequestTarget

View File

@@ -0,0 +1,80 @@
/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sofia Rodrigues
-/
module
prelude
public import Init.Data.Nat
public section
/-!
# URI Parser Configuration
This module defines `URI.Config`, which controls per-component size limits used during URI
parsing. All limits default to the values previously hardcoded in the parser, except
`maxQueryLength`, which is raised from 1024 to 8192 to match `H1.Config.maxUriLength` and
accommodate real-world query strings.
-/
namespace Std.Http.URI
set_option linter.all true
/--
Per-component size limits for the URI parser.
-/
structure Config where
/--
Maximum length of the URI scheme component (e.g. `https`), in bytes.
The scheme grammar requires at least one leading ALPHA; the remaining budget is
`max(0, maxSchemeLength - 1)` additional characters.
-/
maxSchemeLength : Nat := 13
/--
Maximum length of the host `reg-name` component, in bytes.
-/
maxHostLength : Nat := 253
/--
Maximum length of the userinfo component (username and password each), in bytes.
-/
maxUserInfoLength : Nat := 1024
/--
Maximum length of a single path segment, in bytes.
-/
maxSegmentLength : Nat := 256
/--
Maximum length of the query string, in bytes.
Raised from the previously hardcoded 1024 to 8192 to match `H1.Config.maxUriLength`
and allow real-world query strings.
-/
maxQueryLength : Nat := 8192
/--
Maximum length of the fragment component, in bytes.
-/
maxFragmentLength : Nat := 1024
/--
Maximum number of path segments.
Prevents excessive segment counts that could arise from paths like `/a/b/c/…` repeated many times.
-/
maxPathSegments : Nat := 128
/--
Maximum total byte length of the path (all segments combined, including separating slashes).
-/
maxTotalPathLength : Nat := 8192
/--
Maximum number of query parameters (key-value pairs separated by `&`).
-/
maxQueryParams : Nat := 100
end Std.Http.URI

View File

@@ -0,0 +1,688 @@
/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sofia Rodrigues
-/
module
prelude
import Init.Grind
import Init.While
import Init.Data.SInt.Lemmas
import Init.Data.UInt.Lemmas
import Init.Data.UInt.Bitwise
import Init.Data.Array.Lemmas
public import Init.Data.String
public import Std.Internal.Http.Internal.Char
public section
/-!
# URI Encoding
This module provides utilities for percent-encoding URI components according to RFC 3986. It includes
character validation, encoding/decoding functions, and types that maintain encoding invariants through
Lean's dependent type system.
Reference: https://www.rfc-editor.org/rfc/rfc3986.html#section-2.1
-/
namespace Std.Http.URI
set_option linter.all true
open Internal Char
/--
Checks if a byte is a valid character in a percent-encoded URI component. Valid characters are
unreserved characters or the percent sign (for escape sequences).
-/
def isEncodedChar (rule : UInt8 Bool) (c : UInt8) : Bool :=
isAsciiByte c (rule c isHexDigitByte c c = '%'.toUInt8)
/--
Checks if a byte is valid in a percent-encoded query string component. Extends `isEncodedChar` to also
allow '+' which represents space in application/x-www-form-urlencoded format.
-/
def isEncodedQueryChar (rule : UInt8 Bool) (c : UInt8) : Bool :=
isEncodedChar rule c c = '+'.toUInt8
/--
Checks if all characters in a `ByteArray` are allowed in an encoded URI component. This is a fast check
that only verifies the character set, not full encoding validity.
-/
@[inline]
abbrev IsAllowedEncodedChars (rule : UInt8 Bool) (s : ByteArray) : Prop :=
s.data.all (isEncodedChar rule)
instance : Decidable (IsAllowedEncodedChars r s) :=
inferInstanceAs (Decidable (s.data.all (isEncodedChar r) = true))
/--
Checks if all characters in a `ByteArray` are allowed in an encoded query parameter. Allows '+' as an
alternative encoding for space (application/x-www-form-urlencoded).
-/
@[inline]
abbrev IsAllowedEncodedQueryChars (rule : UInt8 Bool) (s : ByteArray) : Prop :=
s.data.all (isEncodedQueryChar rule)
instance : Decidable (IsAllowedEncodedQueryChars r s) :=
inferInstanceAs (Decidable (s.data.all (isEncodedQueryChar r) = true))
/--
Validates that all percent signs in a byte array are followed by exactly two hexadecimal digits.
This ensures proper percent-encoding according to RFC 3986.
For example:
- `%20` is valid (percent followed by two hex digits)
- `%` is invalid (percent with no following digits)
- `%2` is invalid (percent followed by only one digit)
- `%GG` is invalid (percent followed by non-hex characters)
-/
def isValidPercentEncoding (ba : ByteArray) : Bool :=
let rec loop (i : Nat) : Bool :=
if h : i < ba.size then
let c := ba[i]'h
if c = '%'.toUInt8 then
if h₂ : i + 2 < ba.size then
let d1 := ba[i + 1]'(by omega)
let d2 := ba[i + 2]'h₂
if isHexDigitByte d1 && isHexDigitByte d2 then
loop (i + 3)
else false
else false
else loop (i + 1)
else true
termination_by ba.size - i
loop 0
/--
Converts a nibble (4-bit value, 0-15) to its hexadecimal digit representation. Returns '0'-'9' for
values 0-9, and 'A'-'F' for values 10-15.
-/
def hexDigit (n : UInt8) : UInt8 :=
if n < 10 then (n + '0'.toUInt8)
else (n - 10 + 'A'.toUInt8)
/--
Converts a hexadecimal digit character to its numeric value (0-15).
Returns `none` if the character is not a valid hex digit.
-/
def hexDigitToUInt8? (c : UInt8) : Option UInt8 :=
if c '0'.toUInt8 && c '9'.toUInt8 then
some (c - '0'.toUInt8)
else if c 'a'.toUInt8 && c 'f'.toUInt8 then
some (c - 'a'.toUInt8 + 10)
else if c 'A'.toUInt8 && c 'F'.toUInt8 then
some (c - 'A'.toUInt8 + 10)
else
none
private theorem IsAllowedEncodedChars.push {bs : ByteArray} (h : IsAllowedEncodedChars r bs) (h₁ : isEncodedChar r c) :
IsAllowedEncodedChars r (bs.push c) := by
simpa [IsAllowedEncodedChars, ByteArray.push, Array.all_push, And.intro h h₁]
private theorem IsAllowedEncodedQueryChars.push {bs : ByteArray} (h : IsAllowedEncodedQueryChars r bs) (h₁ : isEncodedQueryChar r c) :
IsAllowedEncodedQueryChars r (bs.push c) := by
simpa [IsAllowedEncodedQueryChars, ByteArray.push, Array.all_push, And.intro h h₁]
private theorem isEncodedChar_isAscii (c : UInt8) (h : isEncodedChar r c) : isAsciiByte c := by
simp [isEncodedChar, isAsciiByte] at *
exact h.left
private theorem isEncodedQueryChar_isAscii (c : UInt8) (h : isEncodedQueryChar r c) : isAsciiByte c := by
unfold isEncodedQueryChar isAsciiByte at *
simp at h
rcases h
next h => exact isEncodedChar_isAscii c h
next h => subst_vars; decide
private theorem hexDigit_isHexDigit (h₀ : x < 16) : isHexDigitByte (hexDigit x) := by
unfold hexDigit isHexDigitByte
have h₁ : x.toNat < 16 := h₀
split <;> simp
next p =>
have h₂ : x.toNat < 10 := p
have h₂ : 48 x.toNat + 48 := by omega
have h₃ : x.toNat + 48 57 := by omega
have h₄ : x.toNat + 48 < 256 := by omega
refine Or.inl (Or.inl ?_, ?_)
· exact (UInt8.ofNat_le_iff_le (by decide) h₄ |>.mpr h₂)
· exact (UInt8.ofNat_le_iff_le h₄ (by decide) |>.mpr h₃)
next p =>
have h₂ : ¬(x.toNat < 10) := p
have h₃ : 65 x.toNat - 10 + 65 := by omega
have h₅ : x.toNat - 10 + 65 70 := by omega
have h₄ : x.toNat - 10 + 65 < 256 := by omega
refine Or.inr ?_, ?_
· simpa [UInt8.ofNat_sub (by omega : 10 x.toNat)] using
UInt8.ofNat_le_iff_le (by decide : 65 < 256) h₄ |>.mpr h₃
· simpa [UInt8.ofNat_add, UInt8.ofNat_sub (by omega : 10 x.toNat)] using
UInt8.ofNat_le_iff_le h₄ (by decide : 70 < 256) |>.mpr h₅
private theorem isHexDigit_isAscii {c : UInt8} (h : isHexDigitByte c) : isAsciiByte c := by
simp [isHexDigitByte, isAsciiByte] at *
rcases h with h1, h2 | h1, h2
· exact UInt8.lt_of_le_of_lt h2 (by decide)
next h => exact UInt8.lt_of_le_of_lt h.right (by decide)
· exact UInt8.lt_of_le_of_lt h2 (by decide)
private theorem isHexDigit_isEncodedChar {c : UInt8} (h : isHexDigitByte c) : isEncodedChar r c := by
unfold isEncodedChar
simp at *
exact And.intro (isHexDigit_isAscii h) (Or.inr (Or.inl h))
private theorem isHexDigit_isEncodedQueryChar {c : UInt8} (h : isHexDigitByte c) : isEncodedQueryChar r c := by
unfold isEncodedQueryChar isEncodedChar
simp at *
exact Or.inl (And.intro (isHexDigit_isAscii h) (Or.inr (Or.inl h)))
theorem all_of_all_of_imp {b : ByteArray} (h : b.data.all p) (imp : c, p c q c) : b.data.all q := by
rw [Array.all_eq] at *
simp at *
intro i x
exact (imp b.data[i]) (h i x)
private theorem autf8EncodeChar_flatMap_ascii {a : List UInt8}
(is_ascii_list : (x : UInt8), x a x < 128) :
List.flatMap (fun a => String.utf8EncodeChar (Char.ofUInt8 a)) a = a := by
have h_encode {i : UInt8} (h : i < 128) : String.utf8EncodeChar (Char.ofUInt8 i) = [i] := by
simp [Char.ofUInt8, String.utf8EncodeChar, show ¬127 < i.toNat from Nat.not_lt_of_le (Nat.le_pred_of_lt h)]
induction a with
| nil => simp
| cons head tail ih =>
simp [List.flatMap_cons]
rw [h_encode]
· simp
rw [ih]
intro x hx
exact is_ascii_list x (by simp [hx])
· exact is_ascii_list head (by simp)
private theorem List.toByteArray_loop_eq (xs : List UInt8) (acc : ByteArray) :
(List.toByteArray.loop xs acc).data = acc.data ++ xs.toArray := by
induction xs generalizing acc with
| nil => simp [List.toByteArray.loop]
| cons x xs ih => simp [List.toByteArray.loop, ih, Array.push]
private theorem ByteArray.toList_toByteArray (ba : ByteArray) :
ba.data.toList.toByteArray = ba := by
cases ba with
| mk data =>
simp [List.toByteArray]
apply ByteArray.ext
simp [List.toByteArray_loop_eq, ByteArray.empty]
decide
theorem isValidUTF8_of_isAsciiByte (ba : ByteArray) (s : ba.data.all isAsciiByte) : ByteArray.IsValidUTF8 ba := by
refine ba.data.toList.map Char.ofUInt8, ?_
rw [List.utf8Encode]
simp only [List.flatMap_map]
have is_ascii : (x : UInt8), x ba.data.toList x < 128 := by
let is_ascii := Array.all_eq_true_iff_forall_mem.mp s
simp [isAsciiByte] at is_ascii
intro x hx
exact is_ascii x (by simp_all)
rw [autf8EncodeChar_flatMap_ascii is_ascii]
exact ByteArray.toList_toByteArray ba |>.symm
/--
A percent-encoded URI component with a compile-time proof that it contains only valid encoded characters.
This provides type-safe URI encoding without runtime validation.
The invariant guarantees that the string contains only unreserved characters (alphanumeric, hyphen, period,
underscore, tilde) and percent signs (for escape sequences).
-/
structure EncodedString (r : UInt8 Bool) where
private mk ::
/--
The underlying byte array containing the percent-encoded data.
-/
toByteArray : ByteArray
/--
Proof that all characters in the byte array are valid encoded characters.
-/
valid : IsAllowedEncodedChars r toByteArray
namespace EncodedString
/--
Creates an empty encoded string.
-/
def empty : EncodedString r :=
.empty, by simp []; exact fun i h => by contradiction
instance : Inhabited (EncodedString r) where
default := EncodedString.empty
/--
Appends a single encoded character to an encoded string.
Requires that the character is not '%' to maintain the percent-encoding invariant.
-/
private def push (s : EncodedString r) (c : UInt8) (h : isEncodedChar r c) : EncodedString r :=
s.toByteArray.push c, IsAllowedEncodedChars.push s.valid h
/--
Converts a byte to its percent-encoded hexadecimal representation (%XX). For example, a space
character (0x20) becomes "%20".
-/
private def byteToHex (b : UInt8) (s : EncodedString r) : EncodedString r :=
let ba := s.toByteArray.push '%'.toUInt8
|>.push (hexDigit (b >>> 4))
|>.push (hexDigit (b &&& 0xF))
let valid := by
have h1 : isEncodedChar r '%'.toUInt8 :=
by simp [isEncodedChar]; decide
have h2 : isEncodedChar r (hexDigit (b >>> 4)) :=
let h₀ := hexDigit_isHexDigit (BitVec.toNat_ushiftRight_lt b.toBitVec 4 (by decide))
isHexDigit_isEncodedChar h₀
have h3 : isEncodedChar r (hexDigit (b &&& 0xF)) :=
let h₀ := hexDigit_isHexDigit (@UInt8.and_lt_add_one b 0xF (by decide))
isHexDigit_isEncodedChar h₀
exact IsAllowedEncodedChars.push (IsAllowedEncodedChars.push (IsAllowedEncodedChars.push s.valid h1) h2) h3
ba, valid
/--
Encodes a raw string into an `EncodedString` with automatic proof construction. Unreserved characters
(alphanumeric, hyphen, period, underscore, tilde) are kept as-is, while all other characters are percent-encoded.
-/
def encode (s : String) : EncodedString r :=
s.toUTF8.foldl (init := EncodedString.empty) fun acc c =>
if h : isAsciiByte c r c then
acc.push c (by simp [isEncodedChar]; exact And.intro h.left (Or.inl h.right))
else
byteToHex c acc
/--
Attempts to create an `EncodedString` from a `ByteArray`. Returns `some` if the byte array contains only
valid encoded characters and all percent signs are followed by exactly two hex digits, `none` otherwise.
-/
def ofByteArray? (ba : ByteArray) : Option (EncodedString r) :=
if h : IsAllowedEncodedChars r ba then
if isValidPercentEncoding ba then some ba, h else none
else none
/--
Creates an `EncodedString` from a `ByteArray`, panicking if the byte array is invalid.
-/
def ofByteArray! (ba : ByteArray) : EncodedString r :=
match ofByteArray? ba with
| some es => es
| none => panic! "invalid encoded string"
/--
Creates an `EncodedString` from a `String` by checking if it's already a valid percent-encoded string.
Returns `some` if valid, `none` otherwise.
-/
def ofString? (s : String) : Option (EncodedString r) :=
ofByteArray? s.toUTF8
/--
Creates an `EncodedString` from a `String`, panicking if the string is not a valid percent-encoded string.
-/
def ofString! (s : String) : EncodedString r :=
ofByteArray! s.toUTF8
/--
Creates an `EncodedString` from a `ByteArray` with compile-time proofs.
Use this when you have proofs that the byte array is valid.
-/
def new (ba : ByteArray) (valid : IsAllowedEncodedChars r ba) (_validEncoding : isValidPercentEncoding ba) : EncodedString r :=
ba, valid
instance : ToString (EncodedString r) where
toString es := es.toByteArray, isValidUTF8_of_isAsciiByte es.toByteArray (all_of_all_of_imp es.valid (fun c h => by simp [isEncodedChar] at h; exact h.left))
/--
Decodes an `EncodedString` back to a regular `String`. Converts percent-encoded sequences (e.g., "%20")
back to their original characters. Returns `none` if the decoded bytes are not valid UTF-8.
-/
def decode (es : EncodedString r) : Option String := Id.run do
let mut decoded : ByteArray := ByteArray.empty
let rawBytes := es.toByteArray
let len := rawBytes.size
let mut i := 0
let percent := '%'.toNat.toUInt8
while h : i < len do
let c := rawBytes[i]
(decoded, i) := if h₁ : c == percent i + 1 < len then
let h1 := rawBytes[i + 1]
if let some hd1 := hexDigitToUInt8? h1 then
if h₂ : i + 2 < len then
let h2 := rawBytes[i + 2]
if let some hd2 := hexDigitToUInt8? h2 then
(decoded.push (hd1 * 16 + hd2), i + 3)
else
(((decoded.push c).push h1).push h2, i + 3)
else
((decoded.push c).push h1, i + 2)
else
((decoded.push c).push h1, i + 2)
else
(decoded.push c, i + 1)
return String.fromUTF8? decoded
instance : Repr (EncodedString r) where
reprPrec es n := reprPrec (toString es) n
instance : BEq (EncodedString r) where
beq x y := x.toByteArray = y.toByteArray
instance : Hashable (EncodedString r) where
hash x := Hashable.hash x.toByteArray
end EncodedString
/--
A percent-encoded query string component with a compile-time proof that it contains only valid encoded
query characters. Extends `EncodedString` to support the '+' character for spaces, following the
application/x-www-form-urlencoded format.
This type is specifically designed for encoding query parameters where spaces can be represented as '+'
instead of "%20".
-/
structure EncodedQueryString (r : UInt8 Bool) where
private mk ::
/--
The underlying byte array containing the percent-encoded query data.
-/
toByteArray : ByteArray
/--
Proof that all characters in the byte array are valid encoded query characters.
-/
valid : IsAllowedEncodedQueryChars r toByteArray
namespace EncodedQueryString
/--
Creates an empty encoded query string.
-/
def empty : EncodedQueryString r :=
.empty, by simp; intro a h; contradiction
instance : Inhabited (EncodedQueryString r) where
default := EncodedQueryString.empty
/--
Appends a single encoded query character to an encoded query string.
-/
private def push (s : EncodedQueryString r) (c : UInt8) (h : isEncodedQueryChar r c) : EncodedQueryString r :=
s.toByteArray.push c, IsAllowedEncodedQueryChars.push s.valid h
/--
Attempts to create an `EncodedQueryString` from a `ByteArray`. Returns `some` if the byte array contains
only valid encoded query characters and all percent signs are followed by exactly two hex digits, `none` otherwise.
-/
def ofByteArray? (ba : ByteArray) (r : UInt8 Bool := isQueryChar) : Option (EncodedQueryString r) :=
if h : IsAllowedEncodedQueryChars r ba then
if isValidPercentEncoding ba then some ba, h else none
else none
/--
Creates an `EncodedQueryString` from a `ByteArray`, panicking if the byte array is invalid.
-/
def ofByteArray! (ba : ByteArray) (r : UInt8 Bool := isQueryChar) : EncodedQueryString r :=
match ofByteArray? ba r with
| some es => es
| none => panic! "invalid encoded query string"
/--
Creates an `EncodedQueryString` from a `String` by checking if it's already a valid percent-encoded string.
Returns `some` if valid, `none` otherwise.
-/
def ofString? (s : String) (r : UInt8 Bool := isQueryChar) : Option (EncodedQueryString r) :=
ofByteArray? s.toUTF8 r
/--
Creates an `EncodedQueryString` from a `String`, panicking if the string is not a valid percent-encoded string.
-/
def ofString! (s : String) (r : UInt8 Bool := isQueryChar) : EncodedQueryString r :=
ofByteArray! s.toUTF8 r
/--
Creates an `EncodedQueryString` from a `ByteArray` with compile-time proofs.
Use this when you have proofs that the byte array is valid.
-/
def new (ba : ByteArray) (valid : IsAllowedEncodedQueryChars r ba) (_validEncoding : isValidPercentEncoding ba) : EncodedQueryString r :=
ba, valid
/--
Converts a byte to its percent-encoded hexadecimal representation (%XX). For example, a space character
(0x20) becomes "%20".
-/
private def byteToHex (b : UInt8) (s : EncodedQueryString r) : EncodedQueryString r :=
let ba := s.toByteArray.push '%'.toUInt8
|>.push (hexDigit (b >>> 4))
|>.push (hexDigit (b &&& 0xF))
let valid := by
have h1 : isEncodedQueryChar r '%'.toUInt8 := by
simp [isEncodedQueryChar, isEncodedChar]; decide
have h2 : isEncodedQueryChar r (hexDigit (b >>> 4)) :=
isHexDigit_isEncodedQueryChar (hexDigit_isHexDigit (BitVec.toNat_ushiftRight_lt b.toBitVec 4 (by decide)))
have h3 : isEncodedQueryChar r (hexDigit (b &&& 0xF)) :=
isHexDigit_isEncodedQueryChar (hexDigit_isHexDigit (@UInt8.and_lt_add_one b 0xF (by decide)))
exact IsAllowedEncodedQueryChars.push (IsAllowedEncodedQueryChars.push (IsAllowedEncodedQueryChars.push s.valid h1) h2) h3
ba, valid
/--
Encodes a raw string into an `EncodedQueryString` with automatic proof construction. Unreserved characters
are kept as-is, spaces are encoded as '+', and all other characters are percent-encoded.
-/
def encode (s : String) (r : UInt8 Bool := isQueryChar) : EncodedQueryString r :=
s.toUTF8.foldl (init := EncodedQueryString.empty) fun acc c =>
if h : isAsciiByte c r c then
acc.push c (by simp [isEncodedQueryChar, isEncodedChar]; exact Or.inl (And.intro h.left (Or.inl h.right)))
else if _ : c = ' '.toUInt8 then
acc.push '+'.toUInt8 (by simp [isEncodedQueryChar])
else
byteToHex c acc
/--
Converts an `EncodedQueryString` to a `String`, given a proof that all characters satisfying `r` are ASCII.
-/
def toString (es : EncodedQueryString r) : String :=
es.toByteArray, isValidUTF8_of_isAsciiByte es.toByteArray (all_of_all_of_imp es.valid (fun c h => isEncodedQueryChar_isAscii c h))
/--
Decodes an `EncodedQueryString` back to a regular `String`. Converts percent-encoded sequences and '+'
signs back to their original characters. Returns `none` if the decoded bytes are not valid UTF-8.
This is almost the same code from `System.Uri.UriEscape.decodeUri`, but with `Option` instead.
-/
def decode (es : EncodedQueryString r) : Option String := Id.run do
let mut decoded : ByteArray := ByteArray.empty
let rawBytes := es.toByteArray
let len := rawBytes.size
let mut i := 0
let percent := '%'.toNat.toUInt8
let plus := '+'.toNat.toUInt8
while h : i < len do
let c := rawBytes[i]
(decoded, i) := if c == plus then
(decoded.push ' '.toNat.toUInt8, i + 1)
else if h₁ : c == percent i + 1 < len then
let h1 := rawBytes[i + 1]
if let some hd1 := hexDigitToUInt8? h1 then
if h₂ : i + 2 < len then
let h2 := rawBytes[i + 2]
if let some hd2 := hexDigitToUInt8? h2 then
(decoded.push (hd1 * 16 + hd2), i + 3)
else
(((decoded.push c).push h1).push h2, i + 3)
else
((decoded.push c).push h1, i + 2)
else
((decoded.push c).push h1, i + 2)
else
(decoded.push c, i + 1)
return String.fromUTF8? decoded
end EncodedQueryString
instance : ToString (EncodedQueryString r) where
toString := EncodedQueryString.toString
instance : Repr (EncodedQueryString r) where
reprPrec es n := reprPrec (toString es) n
instance : BEq (EncodedQueryString r) where
beq x y := x.toByteArray = y.toByteArray
instance : Hashable (EncodedQueryString r) where
hash x := Hashable.hash x.toByteArray
instance : Hashable (Option (EncodedQueryString r)) where
hash
| some x => Hashable.hash ((ByteArray.mk #[1] ++ x.toByteArray))
| none => Hashable.hash (ByteArray.mk #[0])
/--
A percent-encoded URI path segment. Valid characters are `pchar` (unreserved, sub-delims, ':', '@').
-/
abbrev EncodedSegment := EncodedString isPChar
namespace EncodedSegment
/--
Encodes a raw string into an encoded path segment.
-/
def encode (s : String) : EncodedSegment :=
EncodedString.encode (r := isPChar) s
/--
Attempts to create an encoded path segment from raw bytes.
-/
def ofByteArray? (ba : ByteArray) : Option EncodedSegment :=
EncodedString.ofByteArray? (r := isPChar) ba
/--
Creates an encoded path segment from raw bytes, panicking on invalid encoding.
-/
def ofByteArray! (ba : ByteArray) : EncodedSegment :=
EncodedString.ofByteArray! (r := isPChar) ba
/--
Decodes an encoded path segment back to a UTF-8 string.
-/
def decode (segment : EncodedSegment) : Option String :=
EncodedString.decode segment
end EncodedSegment
/--
A percent-encoded URI fragment component. Valid characters are `pchar / "/" / "?"`.
-/
abbrev EncodedFragment := EncodedString isFragmentChar
namespace EncodedFragment
/--
Encodes a raw string into an encoded fragment component.
-/
def encode (s : String) : EncodedFragment :=
EncodedString.encode (r := isFragmentChar) s
/--
Attempts to create an encoded fragment component from raw bytes.
-/
def ofByteArray? (ba : ByteArray) : Option EncodedFragment :=
EncodedString.ofByteArray? (r := isFragmentChar) ba
/--
Creates an encoded fragment component from raw bytes, panicking on invalid encoding.
-/
def ofByteArray! (ba : ByteArray) : EncodedFragment :=
EncodedString.ofByteArray! (r := isFragmentChar) ba
/--
Decodes an encoded fragment component back to a UTF-8 string.
-/
def decode (fragment : EncodedFragment) : Option String :=
EncodedString.decode fragment
end EncodedFragment
/--
A percent-encoded URI userinfo component. Valid characters are `unreserved / sub-delims / ":"`.
-/
abbrev EncodedUserInfo := EncodedString isUserInfoChar
namespace EncodedUserInfo
/--
Encodes a raw string into an encoded userinfo component.
-/
def encode (s : String) : EncodedUserInfo :=
EncodedString.encode (r := isUserInfoChar) s
/--
Attempts to create an encoded userinfo component from raw bytes.
-/
def ofByteArray? (ba : ByteArray) : Option EncodedUserInfo :=
EncodedString.ofByteArray? (r := isUserInfoChar) ba
/--
Creates an encoded userinfo component from raw bytes, panicking on invalid encoding.
-/
def ofByteArray! (ba : ByteArray) : EncodedUserInfo :=
EncodedString.ofByteArray! (r := isUserInfoChar) ba
/--
Decodes an encoded userinfo component back to a UTF-8 string.
-/
def decode (userInfo : EncodedUserInfo) : Option String :=
EncodedString.decode userInfo
end EncodedUserInfo
/--
A percent-encoded URI query parameter. Valid characters are `pchar / "/" / "?"` with '+' for spaces.
-/
abbrev EncodedQueryParam := EncodedQueryString isQueryDataChar
namespace EncodedQueryParam
/--
Encodes a raw string into an encoded query parameter.
-/
def encode (s : String) : EncodedQueryParam :=
EncodedQueryString.encode (r := isQueryDataChar) s
/--
Attempts to create an encoded query parameter from raw bytes.
-/
def ofByteArray? (ba : ByteArray) : Option EncodedQueryParam :=
EncodedQueryString.ofByteArray? (r := isQueryDataChar) ba
/--
Creates an encoded query parameter from raw bytes, panicking on invalid encoding.
-/
def ofByteArray! (ba : ByteArray) : EncodedQueryParam :=
EncodedQueryString.ofByteArray! (r := isQueryDataChar) ba
/--
Attempts to create an encoded query parameter from an encoded string.
-/
def fromString? (s : String) : Option EncodedQueryParam :=
EncodedQueryString.ofString? (r := isQueryDataChar) s
/--
Decodes an encoded query parameter back to a UTF-8 string.
-/
def decode (param : EncodedQueryParam) : Option String :=
EncodedQueryString.decode param
end EncodedQueryParam
end Std.Http.URI

View File

@@ -0,0 +1,430 @@
/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sofia Rodrigues
-/
module
prelude
import Init.While
public import Init.Data.String
public import Std.Internal.Parsec
public import Std.Internal.Parsec.ByteArray
public import Std.Internal.Http.Data.URI.Basic
public import Std.Internal.Http.Data.URI.Config
public section
/-!
# URI Parser
This module provides parsers for HTTP request targets and HTTP-oriented URIs aligned with RFC 3986.
It handles parsing of schemes, authorities, paths, queries, and fragments.
Notable intentional constraints:
* hosts are limited to IPv4, bracketed IPv6, and DNS-style domain names
* IPvFuture (`v...`) inside `IP-literal` is currently rejected
References:
* https://www.rfc-editor.org/rfc/rfc3986.html
* https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-references
* https://www.rfc-editor.org/rfc/rfc9112.html#section-3.3
-/
namespace Std.Http.URI.Parser
set_option linter.all true
open Internal Char
open Std Internal Parsec ByteArray
@[inline]
private def tryOpt (p : Parser α) : Parser (Option α) :=
optional (attempt p)
@[inline]
private def peekIs (p : UInt8 Bool) : Parser Bool := do
return ( peekWhen? p).isSome
-- scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
private def parseScheme (config : URI.Config) : Parser URI.Scheme := do
if config.maxSchemeLength = 0 then
fail "scheme length limit is 0 (no scheme allowed)"
let first takeWhileUpTo1 isAlphaByte 1
let rest takeWhileUpTo
(fun c =>
isAlphaNum c
c = '+'.toUInt8 c = '-'.toUInt8 c = '.'.toUInt8)
(config.maxSchemeLength - 1)
let schemeBytes := first.toByteArray ++ rest.toByteArray
let str := String.fromUTF8! schemeBytes |>.toLower
if h : URI.IsValidScheme str then
return str, h
else
fail "invalid scheme"
-- port = 1*DIGIT
private def parsePortNumber : Parser UInt16 := do
let portBytes takeWhileUpTo1 isDigitByte 5
let portStr := String.fromUTF8! portBytes.toByteArray
let some portNum := String.toNat? portStr
| fail s!"invalid port number: {portStr}"
if portNum > 65535 then
fail s!"port number too large: {portNum}"
return portNum.toUInt16
-- userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
private def parseUserInfo (config : URI.Config) : Parser URI.UserInfo := do
let userBytesName takeWhileUpTo
(fun x =>
x ':'.toUInt8
(isUserInfoChar x x = '%'.toUInt8))
config.maxUserInfoLength
let some userNameEncoded := URI.EncodedUserInfo.ofByteArray? userBytesName.toByteArray
| fail "invalid percent encoding in user info"
let userPassEncoded if peekIs (· == ':'.toUInt8) then
skip
let userBytesPass takeWhileUpTo
(fun x => isUserInfoChar x x = '%'.toUInt8)
config.maxUserInfoLength
let some userPassEncoded := URI.EncodedUserInfo.ofByteArray? userBytesPass.toByteArray
| fail "invalid percent encoding in user info"
pure <| some userPassEncoded
else
pure none
return userNameEncoded, userPassEncoded
-- Parses bracketed IPv6 literals.
-- Note: RFC 3986 also allows IPvFuture inside `IP-literal`; this parser
-- currently rejects IPvFuture.
private def parseIPv6 : Parser Net.IPv6Addr := do
skipByte '['.toUInt8
let result takeWhileUpTo1
(fun x => x = ':'.toUInt8 x = '.'.toUInt8 isHexDigitByte x)
256
skipByte ']'.toUInt8
let ipv6Str := String.fromUTF8! result.toByteArray
let some ipv6Addr := Std.Net.IPv6Addr.ofString ipv6Str
| fail s!"invalid IPv6 address: {ipv6Str}"
return ipv6Addr
-- IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
private def parseIPv4 : Parser Net.IPv4Addr := do
let result takeWhileUpTo1
(fun x => x = '.'.toUInt8 isDigitByte x)
256
let ipv4Str := String.fromUTF8! result.toByteArray
let some ipv4Addr := Std.Net.IPv4Addr.ofString ipv4Str
| fail s!"invalid IPv4 address: {ipv4Str}"
return ipv4Addr
-- host = IP-literal / IPv4address / reg-name
-- Note: RFC 1123 allows domain labels to start with digits, so we must try IPv4
-- first and fall back to reg-name parsing if it fails.
private def parseHost (config : URI.Config) : Parser URI.Host := do
if ( peekWhen? (· == '['.toUInt8)).isSome then
return .ipv6 ( parseIPv6)
else
if ( peekWhen? isDigitByte).isSome then
if let some ipv4 tryOpt parseIPv4 then
return .ipv4 ipv4
-- We intentionally parse DNS names here (not full RFC 3986 reg-name).
let some str := String.fromUTF8? ( takeWhileUpTo1
(fun x => isAlphaNum x x = '-'.toUInt8 x = '.'.toUInt8)
config.maxHostLength).toByteArray
| fail s!"invalid host"
let lower := str.toLower
if h : URI.IsValidDomainName lower ¬lower.isEmpty then
return .name lower, .isLowerCase_toLower, h
else
fail s!"invalid domain name: {str}"
-- authority = [ userinfo "@" ] host [ ":" port ]
private def parseAuthority (config : URI.Config) : Parser URI.Authority := do
let userInfo tryOpt do
let ui parseUserInfo config
skipByte '@'.toUInt8
return ui
let host parseHost config
let port : URI.Port
if peekIs (· == ':'.toUInt8) then
skipByte ':'.toUInt8
if ( peekWhen? isDigitByte).isSome then
pure (.value ( parsePortNumber))
else
let next peek?
if next.isNone || next.any (fun c => c = '/'.toUInt8 c = '?'.toUInt8 c = '#'.toUInt8) then
pure .empty
else
fail "invalid port number"
else
pure .omitted
return { userInfo, host, port }
-- segment = *pchar
private def parseSegment (config : URI.Config) : Parser ByteSlice := do
takeWhileUpTo (fun c => isPChar c c = '%'.toUInt8) config.maxSegmentLength
/-
path = path-abempty ; begins with "/" or is empty
/ path-absolute ; begins with "/" but not "//"
/ path-noscheme ; begins with a non-colon segment
/ path-rootless ; begins with a segment
/ path-empty ; zero characters
path-abempty = *( "/" segment )
path-absolute = "/" [ segment-nz *( "/" segment ) ]
path-noscheme = segment-nz-nc *( "/" segment )
path-rootless = segment-nz *( "/" segment )
path-empty = 0<pchar>
-/
/--
Parses a URI path with combined parsing and validation.
-/
def parsePath (config : URI.Config) (forceAbsolute : Bool) (allowEmpty : Bool) : Parser URI.Path := do
let isPathDelimiter : UInt8 Bool := fun c => c = '?'.toUInt8 c = '#'.toUInt8
let mut isAbsolute := false
let mut segments : Array _ := #[]
let mut totalLength := 0
let isSegmentOrSlash
peekIs (fun c => isPChar c c = '%'.toUInt8 c = '/'.toUInt8)
if ¬allowEmpty (( isEof) ¬isSegmentOrSlash) then
fail "need a path"
-- Check if path is absolute
if peekIs (· == '/'.toUInt8) then
isAbsolute := true
totalLength := totalLength + 1
skip
else if forceAbsolute then
if allowEmpty (( isEof) ¬isSegmentOrSlash) then
return { segments := segments, absolute := isAbsolute }
else
fail "require '/' in path"
else
pure ()
-- Parse segments
while ( peek?).isSome do
let some next := ( peek?) | break
if isPathDelimiter next then
break
if ¬(next = '/'.toUInt8 isPChar next next = '%'.toUInt8) then
break
if segments.size >= config.maxPathSegments then
fail s!"too many path segments (limit: {config.maxPathSegments})"
let segmentBytes parseSegment config
let some segmentStr := URI.EncodedSegment.ofByteArray? segmentBytes.toByteArray
| fail "invalid percent encoding in path segment"
totalLength := totalLength + segmentBytes.size
if totalLength > config.maxTotalPathLength then
fail s!"path too long (limit: {config.maxTotalPathLength} bytes)"
segments := segments.push segmentStr
if ( peek?).any (· == '/'.toUInt8) then
totalLength := totalLength + 1
if totalLength > config.maxTotalPathLength then
fail s!"path too long (limit: {config.maxTotalPathLength} bytes)"
skip
-- If path ends with '/', add empty segment
let next peek?
if next.isNone || next.any isPathDelimiter then
if segments.size >= config.maxPathSegments then
fail s!"too many path segments (limit: {config.maxPathSegments})"
segments := segments.push (URI.EncodedString.empty)
else
break
return { segments := segments, absolute := isAbsolute }
-- query = *( pchar / "/" / "?" )
private def parseQuery (config : URI.Config) : Parser URI.Query := do
let queryBytes
takeWhileUpTo (fun c => isQueryChar c c = '%'.toUInt8) config.maxQueryLength
let some queryStr := String.fromUTF8? queryBytes.toByteArray
| fail "invalid query string"
if queryStr.isEmpty then
return URI.Query.empty
let rawPairs := queryStr.splitOn "&"
if rawPairs.length > config.maxQueryParams then
fail s!"too many query parameters (limit: {config.maxQueryParams})"
let pairs : Option URI.Query := rawPairs.foldlM (init := URI.Query.empty) fun acc pair => do
match pair.splitOn "=" with
| [key] =>
let key URI.EncodedQueryParam.fromString? key
pure (acc.insertEncoded key none)
| key :: value =>
let key URI.EncodedQueryParam.fromString? key
let value URI.EncodedQueryParam.fromString? (String.intercalate "=" value)
pure (acc.insertEncoded key (some value))
| [] => pure acc -- unreachable: splitOn always returns at least one element
if let some pairs := pairs then
return pairs
else
fail "invalid query string"
-- fragment = *( pchar / "/" / "?" )
private def parseFragment (config : URI.Config) : Parser URI.EncodedFragment := do
let fragmentBytes
takeWhileUpTo (fun c => isFragmentChar c c = '%'.toUInt8) config.maxFragmentLength
let some fragmentStr := URI.EncodedFragment.ofByteArray? fragmentBytes.toByteArray
| fail "invalid percent encoding in fragment"
return fragmentStr
private def parseHierPart (config : URI.Config) : Parser (Option URI.Authority × URI.Path) := do
-- Check for "//" authority path-abempty
if ( tryOpt (skipString "//")).isSome then
let authority parseAuthority config
let path parsePath config true true -- path-abempty (must start with "/" or be empty)
return (some authority, path)
else
-- path-absolute / path-rootless / path-empty
let path parsePath config false true
return (none, path)
/--
Parses a URI (Uniform Resource Identifier).
URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
hier-part = "//" authority path-abempty / path-absolute / path-rootless / path-empty
-/
public def parseURI (config : URI.Config := {}) : Parser URI := do
let scheme parseScheme config
skipByte ':'.toUInt8
let (authority, path) parseHierPart config
let query optional (skipByteChar '?' *> parseQuery config)
let query := query.getD .empty
let fragment optional do
let some result := ( (skipByteChar '#' *> parseFragment config)) |>.decode
| fail "invalid fragment parse encoding"
return result
return { scheme, authority, path, query, fragment }
/--
Parses a request target with combined parsing and validation.
-/
public def parseRequestTarget (config : URI.Config := {}) : Parser RequestTarget :=
asterisk <|> origin <|> absoluteHttp <|> authority <|> absolute
where
-- The asterisk form
asterisk : Parser RequestTarget := do
skipByte '*'.toUInt8
return .asteriskForm
-- origin-form = absolute-path [ "?" query ]
-- absolute-path = 1*( "/" segment )
origin : Parser RequestTarget := attempt do
if peekIs (· == '/'.toUInt8) then
let path parsePath config true true
let query optional (skipByte '?'.toUInt8 *> parseQuery config)
return .originForm path query
else
fail "not origin"
absoluteFromScheme (scheme : URI.Scheme) : Parser RequestTarget := do
skipByte ':'.toUInt8
let (auth, path) parseHierPart config
let query optional (skipByteChar '?' *> parseQuery config)
let query := query.getD URI.Query.empty
return .absoluteForm { path, scheme, authority := auth, query, fragment := none }
-- Prefer absolute-form for explicit HTTP(S) scheme targets with a path or authority.
-- This avoids misclassifying `http://host/path` as authority-form while still
-- letting `http:80` fall through to authority-form parsing.
absoluteHttp : Parser RequestTarget := attempt do
let scheme parseScheme config
if scheme.val = "http" || scheme.val = "https" then
skipByte ':'.toUInt8
if peekIs (· == '/'.toUInt8) then
let (authority, path) parseHierPart config
let query optional (skipByteChar '?' *> parseQuery config)
let query := query.getD .empty
return .absoluteForm { scheme, path, authority, query, fragment := none }
else
fail "not http absolute uri with path"
else
fail "not http absolute uri"
-- absolute-URI = scheme ":" hier-part [ "?" query ]
absolute : Parser RequestTarget := attempt do
let scheme parseScheme config
absoluteFromScheme scheme
-- authority-form = host ":" port
authority : Parser RequestTarget := attempt do
let host parseHost config
skipByteChar ':'
let port parsePortNumber
return .authorityForm { host, port := .value port }
/--
Parses an HTTP `Host` header value.
-/
public def parseHostHeader (config : URI.Config := {}) : Parser (URI.Host × URI.Port) := do
let host parseHost config
let port : URI.Port
if peekIs (· == ':'.toUInt8) then
skipByte ':'.toUInt8
if ( peekWhen? isDigitByte).isSome then
pure (.value ( parsePortNumber))
else
let next peek?
if next.isNone then
pure .empty
else
fail "invalid host header port"
else
pure .omitted
if ¬( isEof) then
fail "invalid host header"
return (host, port)
end Std.Http.URI.Parser

View File

@@ -299,4 +299,15 @@ that provides it.
def isUserInfoChar (c : UInt8) : Bool :=
isUnreserved c || isSubDelims c || c = ':'.toUInt8
/--
Checks if a byte is a valid character in a URI query component,
excluding the typical key/value separators `&` and `=`.
Inspired by `query = *( pchar / "/" / "?" )` from RFC 3986,
but disallows `&` and `=` so they can be treated as structural separators.
-/
@[inline, expose]
def isQueryDataChar (c : UInt8) : Bool :=
isQueryChar c && c ≠ '&'.toUInt8 && c ≠ '='.toUInt8
end Std.Http.Internal.Char

View File

@@ -141,13 +141,13 @@ info: "X-Custom-Header: value\x0d\n"
info: "GET /path HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr ({ method := .get, version := .v11, uri := "/path" } : Request.Head)
#eval encodeStr ({ method := .get, version := .v11, uri := .parse! "/path" } : Request.Head)
/--
info: "POST /submit HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr ({ method := .post, version := .v11, uri := "/submit" } : Request.Head)
#eval encodeStr ({ method := .post, version := .v11, uri := .parse! "/submit" } : Request.Head)
/--
info: "PUT /resource HTTP/2.0\x0d\nContent-Type: application/json\x0d\n\x0d\n"
@@ -156,7 +156,7 @@ info: "PUT /resource HTTP/2.0\x0d\nContent-Type: application/json\x0d\n\x0d\n"
#eval encodeStr ({
method := .put
version := .v20
uri := "/resource"
uri := .parse! "/resource"
headers := Headers.empty.insert! "content-type" "application/json"
} : Request.Head)
@@ -222,61 +222,61 @@ info: "a\x0d\n0123456789\x0d\n"
info: "GET /index.html HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr (Request.get "/index.html" |>.body ()).line
#eval encodeStr (Request.get (.parse! "/index.html") |>.body ()).line
/--
info: "POST /api/data HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr (Request.post "/api/data" |>.body ()).line
#eval encodeStr (Request.post (.parse! "/api/data") |>.body ()).line
/--
info: "PUT /resource HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr (Request.put "/resource" |>.body ()).line
#eval encodeStr (Request.put (.parse! "/resource") |>.body ()).line
/--
info: "DELETE /item HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr (Request.delete "/item" |>.body ()).line
#eval encodeStr (Request.delete (.parse! "/item") |>.body ()).line
/--
info: "PATCH /update HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr (Request.patch "/update" |>.body ()).line
#eval encodeStr (Request.patch (.parse! "/update") |>.body ()).line
/--
info: "HEAD /check HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr (Request.head "/check" |>.body ()).line
#eval encodeStr (Request.head (.parse! "/check") |>.body ()).line
/--
info: "OPTIONS * HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr (Request.options "*" |>.body ()).line
#eval encodeStr (Request.options (.parse! "*") |>.body ()).line
/--
info: "CONNECT proxy:8080 HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr (Request.connect "proxy:8080" |>.body ()).line
#eval encodeStr (Request.connect (.parse! "proxy:8080") |>.body ()).line
/--
info: "TRACE /debug HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr (Request.trace "/debug" |>.body ()).line
#eval encodeStr (Request.trace (.parse! "/debug") |>.body ()).line
/--
info: "POST /v2 HTTP/2.0\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr (Request.new |>.method .post |>.uri "/v2" |>.version .v20 |>.body ()).line
#eval encodeStr (Request.new |>.method .post |>.uri (.parse! "/v2") |>.version .v20 |>.body ()).line
/-! ## Response builder -/
@@ -578,35 +578,14 @@ info: true
info: "GET /search?q=hello&lang=en HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr ({ method := .get, version := .v11, uri := "/search?q=hello&lang=en" } : Request.Head)
-- URI with fragment
/--
info: "GET /page#section HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr ({ method := .get, version := .v11, uri := "/page#section" } : Request.Head)
#eval encodeStr ({ method := .get, version := .v11, uri := .parse! "/search?q=hello&lang=en" } : Request.Head)
-- URI with percent-encoded characters
/--
info: "GET /path%20with%20spaces HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr ({ method := .get, version := .v11, uri := "/path%20with%20spaces" } : Request.Head)
-- URI with special characters (brackets, colons)
/--
info: "GET /api/v1/users/[id]:action HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr ({ method := .get, version := .v11, uri := "/api/v1/users/[id]:action" } : Request.Head)
-- Empty URI
/--
info: "GET HTTP/1.1\x0d\n\x0d\n"
-/
#guard_msgs in
#eval encodeStr ({ method := .get, version := .v11, uri := "" } : Request.Head)
#eval encodeStr ({ method := .get, version := .v11, uri :=.parse! "/path%20with%20spaces" } : Request.Head)
/-! ## Edge cases: Response with unusual statuses -/

View File

@@ -0,0 +1,927 @@
/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sofia Rodrigues
-/
import Std.Internal.Http.Data.URI
import Std.Internal.Http.Data.URI.Encoding
open Std.Http
open Std.Http.URI
open Std.Http.URI.Parser
/-!
# URI Tests
Comprehensive tests for URI parsing, encoding, normalization, and manipulation.
This file consolidates tests from multiple URI-related test files.
-/
-- ============================================================================
-- Helper Functions
-- ============================================================================
def runParser (parser : Std.Internal.Parsec.ByteArray.Parser α) (s : String) : IO α :=
IO.ofExcept ((parser <* Std.Internal.Parsec.eof).run s.toUTF8)
def parseCheck (s : String) (exact : String := s) : IO Unit := do
let result runParser parseRequestTarget s
if toString result = exact then
pure ()
else
throw (.userError s!"expect {exact.quote} but got {(toString result).quote}")
def parseCheckFail (s : String) : IO Unit := do
match (parseRequestTarget <* Std.Internal.Parsec.eof).run s.toUTF8 with
| .ok r =>
throw <| .userError
s!"expected parse failure, but succeeded with {(repr r)}"
| .error _ =>
pure ()
-- ============================================================================
-- Percent Encoding Tests (EncodedString)
-- ============================================================================
-- Valid percent encoding validation
/--
info: some "abc"
-/
#guard_msgs in
#eval IO.println (repr (EncodedSegment.ofByteArray? "abc".toUTF8))
/--
info: some "%20"
-/
#guard_msgs in
#eval IO.println (repr (EncodedSegment.ofByteArray? "%20".toUTF8))
/--
info: some "hello%20world"
-/
#guard_msgs in
#eval IO.println (repr (EncodedSegment.ofByteArray? "hello%20world".toUTF8))
/--
info: some "%FF"
-/
#guard_msgs in
#eval IO.println (repr (EncodedSegment.ofByteArray? "%FF".toUTF8))
/--
info: some "%00"
-/
#guard_msgs in
#eval IO.println (repr (EncodedSegment.ofByteArray? "%00".toUTF8))
-- Invalid percent encoding: incomplete
/--
info: none
-/
#guard_msgs in
#eval IO.println (repr (EncodedSegment.ofByteArray? "%".toUTF8))
/--
info: none
-/
#guard_msgs in
#eval IO.println (repr (EncodedSegment.ofByteArray? "hello%".toUTF8))
/--
info: none
-/
#guard_msgs in
#eval IO.println (repr (EncodedSegment.ofByteArray? "%2".toUTF8))
/--
info: none
-/
#guard_msgs in
#eval IO.println (repr (EncodedSegment.ofByteArray? "%A".toUTF8))
-- Invalid percent encoding: non-hex characters
/--
info: none
-/
#guard_msgs in
#eval IO.println (repr (EncodedSegment.ofByteArray? "%GG".toUTF8))
/--
info: none
-/
#guard_msgs in
#eval IO.println (repr (EncodedSegment.ofByteArray? "%2G".toUTF8))
/--
info: none
-/
#guard_msgs in
#eval IO.println (repr (EncodedSegment.ofByteArray? "%G2".toUTF8))
-- ============================================================================
-- Percent Encoding Decode Tests
-- ============================================================================
/--
info: some "abc"
-/
#guard_msgs in
#eval IO.println (repr <| EncodedSegment.decode =<< (EncodedSegment.ofByteArray? "abc".toUTF8))
/--
info: some " "
-/
#guard_msgs in
#eval IO.println (repr <| EncodedSegment.decode =<< (EncodedSegment.ofByteArray? "%20".toUTF8))
/--
info: some "hello world"
-/
#guard_msgs in
#eval IO.println (repr <| EncodedSegment.decode =<< (EncodedSegment.ofByteArray? "hello%20world".toUTF8))
/--
info: some " !"
-/
#guard_msgs in
#eval IO.println (repr <| EncodedSegment.decode =<< (EncodedSegment.ofByteArray? "%20%21".toUTF8))
/--
info: none
-/
#guard_msgs in
#eval IO.println (repr <| EncodedSegment.decode =<< (EncodedSegment.ofByteArray? "%FF".toUTF8))
/--
info: some "\x00"
-/
#guard_msgs in
#eval IO.println (repr <| EncodedSegment.decode =<< (EncodedSegment.ofByteArray? "%00".toUTF8))
-- ============================================================================
-- Query String Encoding Tests
-- ============================================================================
/--
info: some "hello+world"
-/
#guard_msgs in
#eval IO.println (repr (EncodedQueryString.ofByteArray? "hello+world".toUTF8))
/--
info: none
-/
#guard_msgs in
#eval IO.println (repr (EncodedQueryString.ofByteArray? "%".toUTF8))
/--
info: some "hello world"
-/
#guard_msgs in
#eval IO.println (repr <| EncodedQueryString.decode =<< (EncodedQueryString.ofByteArray? "hello+world".toUTF8))
/--
info: some " "
-/
#guard_msgs in
#eval IO.println (repr <| EncodedQueryString.decode =<< (EncodedQueryString.ofByteArray? "%20".toUTF8))
-- ============================================================================
-- Request Target Parsing - Basic Tests
-- ============================================================================
#eval parseCheck "///path/with/encoded%20space"
#eval parseCheck "/path/with/encoded%20space"
#eval parseCheck "/path/with/encoded%20space/"
#eval parseCheck "*"
#eval parseCheck "/api/search?q=hello%20world&category=tech%2Bgames"
#eval parseCheck "/"
#eval parseCheck "/api/v1/users/123/posts/456/comments/789"
#eval parseCheck "/files/../etc/passwd"
#eval parseCheck "example.com:8080"
#eval parseCheck "https://example.com:8080/ata"
#eval parseCheck "https://example.com:8080////./ata"
#eval parseCheck "192.168.1.1:3000"
#eval parseCheck "[::1]:8080"
#eval parseCheck "http://example.com/path/to/resource?query=value"
#eval parseCheck "https://api.example.com:443/v1/users?limit=10"
#eval parseCheck "http://[2001:db8::1]:8080/path"
#eval parseCheck "https://xn--nxasmq6b.xn--o3cw4h/path"
#eval parseCheck "localhost:65535"
#eval parseCheck "http:80"
#eval parseCheck "https://user:pass@secure.example.com/private"
#eval parseCheck "/double//slash//path"
#eval parseCheck "http://user%40example:pass%3Aword@host.com"
#eval parseCheck "http://example.com:/"
#eval parseCheck "http://example.com:/?q=1"
#eval parseCheck "///////"
-- `&` in a key must be percent-encoded so toRawString round-trips correctly.
#guard
let query := URI.Query.empty.insert "a&b" "1"
query.toRawString == "a%26b=1"
-- `=` in a key must be percent-encoded so re-parsing preserves the key.
#guard
let query := URI.Query.empty.insert "a=b" "1"
query.toRawString == "a%3Db=1"
-- `&` in a value must be percent-encoded.
#guard
let query := URI.Query.empty.insert "key" "a&b"
query.toRawString == "key=a%26b"
-- `=` in a value is technically safe (parser uses first `=`), but encoding it
-- is still correct and keeps representation unambiguous.
#guard
let query := URI.Query.empty.insert "key" "a=b"
query.toRawString == "key=a%3Db"
-- Round-trip: insert → toRawString → re-parse should preserve the parameter.
#guard
let original := URI.Query.empty.insert "a&b" "c=d"
let raw := original.toRawString
-- Parse via a synthetic origin-form request target
match (URI.Parser.parseRequestTarget <* Std.Internal.Parsec.eof).run
s!"/path?{raw}".toUTF8 with
| .ok result =>
(result.query.get "a&b" == some "c=d")
| .error _ => false
#guard
match (parseRequestTarget <* Std.Internal.Parsec.eof).run "http:80".toUTF8 with
| .ok (.authorityForm _) => true
| _ => false
-- Parse failure tests
#eval parseCheckFail "/path with space"
#eval parseCheckFail "/path/%"
#eval parseCheckFail "/path/%2"
#eval parseCheckFail "/path/%ZZ"
#eval parseCheckFail ""
#eval parseCheckFail "[::1"
#eval parseCheckFail "[:::1]:80"
#eval parseCheckFail "#frag"
#eval parseCheckFail "/path/\n"
#eval parseCheckFail "/path/\u0000"
#eval parseCheckFail "/page#section"
#eval parseCheckFail "/api/v1/users/[id]:action"
-- maxPathSegments should apply to trailing empty segments as well.
#guard
match (parseURI { maxPathSegments := 1 } <* Std.Internal.Parsec.eof).run
"http://example.com/a/".toUTF8 with
| .error _ => true
| .ok _ => false
-- ============================================================================
-- Request Target Parsing - Detailed Output Tests
-- ============================================================================
/--
info: Std.Http.RequestTarget.originForm { segments := #["path", "with", "encoded%20space"], absolute := true } none
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "/path/with/encoded%20space"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.originForm { segments := #["", "", "path", "with", "encoded%20space"], absolute := true } none
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "///path/with/encoded%20space"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.asteriskForm
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "*"
IO.println (repr result)
/--
info: #[("q", some "hello%20world"), ("category", some "tech%2Bgames")]
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "/api/search?q=hello%20world&category=tech%2Bgames"
IO.println (repr result.query)
/--
info: Std.Http.RequestTarget.originForm { segments := #[], absolute := true } none
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "/"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.authorityForm
{ userInfo := none, host := Std.Http.URI.Host.name "example.com", port := Std.Http.URI.Port.value 8080 }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "example.com:8080"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.authorityForm
{ userInfo := none, host := Std.Http.URI.Host.ipv4 192.168.1.1, port := Std.Http.URI.Port.value 3000 }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "192.168.1.1:3000"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.authorityForm
{ userInfo := none, host := Std.Http.URI.Host.ipv6 ::1, port := Std.Http.URI.Port.value 8080 }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "[::1]:8080"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.absoluteForm
{ scheme := "https",
authority := some { userInfo := none,
host := Std.Http.URI.Host.name "example.com",
port := Std.Http.URI.Port.value 8080 },
path := { segments := #["ata"], absolute := true },
query := #[],
fragment := none }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "https://example.com:8080/ata"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.absoluteForm
{ scheme := "http",
authority := some { userInfo := none,
host := Std.Http.URI.Host.ipv6 2001:db8::1,
port := Std.Http.URI.Port.value 8080 },
path := { segments := #["path"], absolute := true },
query := #[],
fragment := none }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "http://[2001:db8::1]:8080/path"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.absoluteForm
{ scheme := "https",
authority := some { userInfo := some { username := "user%20b", password := some "pass" },
host := Std.Http.URI.Host.name "secure.example.com",
port := Std.Http.URI.Port.omitted },
path := { segments := #["private"], absolute := true },
query := #[],
fragment := none }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "https://user%20b:pass@secure.example.com/private"
IO.println (repr result)
-- ============================================================================
-- IPv6 Host Tests
-- ============================================================================
/--
info: Std.Http.URI.Host.ipv6 ::1
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "[::1]:8080"
match result.authority? with
| some auth => IO.println (repr auth.host)
| none => IO.println "no authority"
/--
info: Std.Http.URI.Host.ipv6 2001:db8::8a2e:370:7334
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "http://[2001:db8::8a2e:370:7334]:8080/api"
match result.authority? with
| some auth => IO.println (repr auth.host)
| none => IO.println "no authority"
/--
info: Std.Http.URI.Host.ipv6 ::
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "http://[::]/path"
match result.authority? with
| some auth => IO.println (repr auth.host)
| none => IO.println "no authority"
-- ============================================================================
-- UserInfo Tests
-- ============================================================================
/--
info: some { username := "user", password := some "pass" }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "https://user:pass@example.com/private"
match result.authority? with
| some auth => IO.println (repr auth.userInfo)
| none => IO.println "no authority"
/--
info: some { username := "user%20only", password := none }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "https://user%20only@example.com/path"
match result.authority? with
| some auth => IO.println (repr auth.userInfo)
| none => IO.println "no authority"
/--
info: some { username := "", password := some "pass" }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "https://:pass@example.com/path"
match result.authority? with
| some auth => IO.println (repr auth.userInfo)
| none => IO.println "no authority"
/--
info: some { username := "user", password := some "p%40ss%3Aw0rd" }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "https://user:p%40ss%3Aw0rd@example.com/"
match result.authority? with
| some auth => IO.println (repr auth.userInfo)
| none => IO.println "no authority"
-- ============================================================================
-- Path.normalize Tests (RFC 3986 Section 5.2.4)
-- ============================================================================
/--
info: /a/b
-/
#guard_msgs in
#eval IO.println <| toString (URI.parse! "http://example.com/a/./b").path.normalize
/--
info: /a
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/a/b/..").path.normalize
/--
info: /a/g
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/a/b/c/./../../g").path.normalize
/--
info: /g
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/../../../g").path.normalize
/--
info: /a/c
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/a/b/../c").path.normalize
/--
info: /a/
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/a/b/c/../.././").path.normalize
/--
info: /
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/a/b/../../..").path.normalize
/--
info: /
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/../../../").path.normalize
/--
info: /a/b/c
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/./a/./b/./c/.").path.normalize
/--
info: /c
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/a/../b/../c").path.normalize
-- ============================================================================
-- Path.parent Tests
-- ============================================================================
/--
info: /a/b
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/a/b/c").path.parent
/--
info: /a
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/a/b").path.parent
/--
info: /
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/a").path.parent
/--
info: /
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/").path.parent
-- ============================================================================
-- Path.join Tests
-- ============================================================================
/--
info: /a/b/c/d
-/
#guard_msgs in
#eval do
let p1 := (URI.parse! "http://example.com/a/b").path
let p2 : URI.Path := { segments := #[URI.EncodedString.encode "c", URI.EncodedString.encode "d"], absolute := false }
IO.println (p1.join p2)
/--
info: /x/y
-/
#guard_msgs in
#eval do
let p1 := (URI.parse! "http://example.com/a/b").path
let p2 : URI.Path := { segments := #[URI.EncodedString.encode "x", URI.EncodedString.encode "y"], absolute := true }
IO.println (p1.join p2)
-- ============================================================================
-- Path.isEmpty Tests
-- ============================================================================
#guard (URI.parse! "http://example.com").path.isEmpty = true
#guard (URI.parse! "http://example.com/").path.absolute = true
#guard (URI.parse! "http://example.com/a").path.isEmpty = false
#guard (URI.parse! "http://example.com/a").path.absolute = true
-- ============================================================================
-- URI Modification Helpers
-- ============================================================================
#guard ((URI.parse! "http://example.com").withScheme! "htTps" |>.scheme) == "https"
#guard ((URI.parse! "http://example.com").withScheme! "ftP" |>.scheme) == "ftp"
/--
info: http://example.com/#section1
-/
#guard_msgs in
#eval IO.println ((URI.parse! "http://example.com/").withFragment (some (toString (URI.EncodedString.encode "section1" : URI.EncodedFragment))))
/--
info: http://example.com/?key=value
-/
#guard_msgs in
#eval do
let uri := URI.parse! "http://example.com/"
let query := URI.Query.empty.insert "key" "value"
IO.println (uri.withQuery query)
/--
info: http://example.com/new/path
-/
#guard_msgs in
#eval do
let uri := URI.parse! "http://example.com/old/path"
let newPath : URI.Path := { segments := #[URI.EncodedString.encode "new", URI.EncodedString.encode "path"], absolute := true }
IO.println (uri.withPath newPath)
-- ============================================================================
-- URI.normalize Tests (RFC 3986 Section 6)
-- ============================================================================
#guard (URI.parse! "HTTP://example.com").normalize.scheme == "http"
#guard (URI.parse! "HtTpS://example.com").normalize.scheme == "https"
/--
info: http://example.com/
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://EXAMPLE.COM/").normalize
/--
info: http://example.com/
-/
#guard_msgs in
#eval IO.println (URI.parse! "HTTP://Example.COM/").normalize
/--
info: http://example.com/a/c
-/
#guard_msgs in
#eval IO.println (URI.parse! "http://example.com/a/b/../c").normalize
/--
info: http://example.com/a/g
-/
#guard_msgs in
#eval IO.println (URI.parse! "HTTP://EXAMPLE.COM/a/b/c/./../../g").normalize
/--
info: https://www.example.com/PATH
-/
#guard_msgs in
#eval IO.println (URI.parse! "HTTPS://WWW.EXAMPLE.COM/PATH").normalize
-- ============================================================================
-- Query Parameter Tests
-- ============================================================================
-- Query with duplicate keys
/--
info: 3
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "/search?tag=a&tag=b&tag=c"
let all := result.query.findAll "tag"
IO.println all.size
/--
info: #[some "a", some "b", some "c"]
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "/search?tag=a&tag=b&tag=c"
let all := result.query.findAll "tag"
IO.println (repr all)
/--
info: some (some "a")
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "/search?key=a&key=b&key=c"
IO.println (repr (result.query.find? "key"))
-- Empty value vs no value
/--
info: some (some "")
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "/api?key="
IO.println (repr (result.query.find? "key"))
/--
info: some none
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "/api?key"
IO.println (repr (result.query.find? "key"))
/--
info: some (some "value")
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "/api?key=value"
IO.println (repr (result.query.find? "key"))
-- Raw lookup APIs should not alias with pre-encoded key spellings.
#guard
match (parseRequestTarget <* Std.Internal.Parsec.eof).run "/api?%61=1&a=2".toUTF8 with
| .ok result =>
let encodedA? := EncodedQueryParam.fromString? "%61"
((result.query.find? "a" |>.bind id |>.bind EncodedQueryParam.decode) == some "2") &&
(result.query.find? "%61").isNone &&
result.query.contains "a" &&
!result.query.contains "%61" &&
(match encodedA? with
| some encodedA =>
((result.query.findEncoded? encodedA |>.bind id |>.bind EncodedQueryParam.decode) == some "1") &&
result.query.containsEncoded encodedA
| none => false)
| .error _ => false
#guard
match (parseRequestTarget <* Std.Internal.Parsec.eof).run "/api?%61=1&a=2".toUTF8 with
| .ok result =>
match EncodedQueryParam.fromString? "%61" with
| some encodedA =>
let erasedRaw := result.query.erase "a"
let erasedEncoded := result.query.eraseEncoded encodedA
!erasedRaw.contains "a" &&
erasedRaw.containsEncoded encodedA &&
!erasedEncoded.containsEncoded encodedA &&
erasedEncoded.contains "a"
| none => false
| .error _ => false
-- ============================================================================
-- Query Operations
-- ============================================================================
#guard (URI.Query.empty.insert "a" "1" |>.contains "a") = true
#guard (URI.Query.empty.contains "nonexistent") = false
/--
info: a=1&b=2
-/
#guard_msgs in
#eval do
let query := URI.Query.empty
|>.insert "a" "1"
|>.insert "b" "2"
IO.println query.toRawString
/--
info: b=2
-/
#guard_msgs in
#eval do
let query := URI.Query.empty
|>.insert "a" "1"
|>.insert "b" "2"
|>.erase "a"
IO.println query.toRawString
/--
info: key=new
-/
#guard_msgs in
#eval do
let query := URI.Query.empty
|>.insert "key" "old"
|>.set "key" "new"
IO.println query.toRawString
-- ============================================================================
-- URI Builder Tests
-- ============================================================================
-- Domain names longer than 255 characters are rejected.
#guard
let label := String.ofList (List.replicate 63 'a')
let longDomain := s!"{label}.{label}.{label}.{label}."
(URI.DomainName.ofString? longDomain).isNone
#guard
let label := String.ofList (List.replicate 63 'a')
let longDomain := s!"{label}.{label}.{label}.{label}."
(URI.Builder.empty.setHost? longDomain).isNone
/--
info: https://example.com/api/users?page=1
-/
#guard_msgs in
#eval do
let uri := URI.Builder.empty
|>.setScheme! "https"
|>.setHost! "example.com"
|>.appendPathSegment "api"
|>.appendPathSegment "users"
|>.addQueryParam "page" "1"
|>.build
IO.println uri
/--
info: http://localhost:8080/
-/
#guard_msgs in
#eval do
let uri := URI.Builder.empty
|>.setScheme! "http"
|>.setHost! "localhost"
|>.setPort 8080
|>.build
IO.println uri
/--
info: https://user:pass@secure.example.com/private
-/
#guard_msgs in
#eval do
let uri := URI.Builder.empty
|>.setScheme! "https"
|>.setUserInfo "user" (some "pass")
|>.setHost! "secure.example.com"
|>.appendPathSegment "private"
|>.build
IO.println uri
-- ============================================================================
-- Encoded Path Segment Tests
-- ============================================================================
/--
info: Std.Http.RequestTarget.originForm { segments := #["path%2Fwith%2Fslashes"], absolute := true } none
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "/path%2Fwith%2Fslashes"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.originForm { segments := #["file%20name.txt"], absolute := true } none
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "/file%20name.txt"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.originForm { segments := #["caf%C3%A9"], absolute := true } none
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "/caf%C3%A9"
IO.println (repr result)
-- ============================================================================
-- Authority Form Tests
-- ============================================================================
/--
info: Std.Http.RequestTarget.authorityForm
{ userInfo := none, host := Std.Http.URI.Host.name "proxy.example.com", port := Std.Http.URI.Port.value 3128 }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "proxy.example.com:3128"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.authorityForm
{ userInfo := none, host := Std.Http.URI.Host.ipv4 127.0.0.1, port := Std.Http.URI.Port.value 8080 }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "127.0.0.1:8080"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.authorityForm
{ userInfo := none, host := Std.Http.URI.Host.name "1example.com", port := Std.Http.URI.Port.value 8080 }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "1example.com:8080"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.absoluteForm
{ scheme := "http",
authority := some { userInfo := none,
host := Std.Http.URI.Host.name "1example.com",
port := Std.Http.URI.Port.omitted },
path := { segments := #["path"], absolute := true },
query := #[],
fragment := none }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "http://1example.com/path"
IO.println (repr result)
/--
info: Std.Http.RequestTarget.absoluteForm
{ scheme := "http",
authority := some { userInfo := none,
host := Std.Http.URI.Host.name "123abc.example.com",
port := Std.Http.URI.Port.omitted },
path := { segments := #["page"], absolute := true },
query := #[],
fragment := none }
-/
#guard_msgs in
#eval show IO _ from do
let result runParser parseRequestTarget "http://123abc.example.com/page"
IO.println (repr result)