Compare commits

..

392 commits
0.1.1 ... main

Author SHA1 Message Date
yggverse
11d17e004e update version 2025-11-07 21:15:20 +02:00
yggverse
bba51e38e8 apply fmt updates 2025-10-19 22:46:49 +03:00
yggverse
0f6eaa563c update version 2025-10-19 22:38:51 +03:00
yggverse
7e9ecf64b3 implement default trait 2025-10-19 22:37:48 +03:00
yggverse
f8537e4ab6 use latest dependencies version 2025-07-23 05:08:13 +03:00
yggverse
5019e66667 use latest 0.20 api 2025-07-23 04:29:01 +03:00
yggverse
d8e0a8e35a update dependencies version 2025-07-23 03:07:24 +03:00
yggverse
c5d10e020a return Connection on Request error 2025-07-23 01:27:20 +03:00
yggverse
e878fe4ba2 return NetworkAddress on Error::Connect 2025-07-22 09:49:39 +03:00
yggverse
cc1018224a reorganize error types, return socket_connection on init error 2025-07-22 09:46:32 +03:00
yggverse
44196608ce implement optional TOFU validation 2025-07-22 08:48:56 +03:00
yggverse
bb5b1dfb53 apply clippy optimizations 2025-07-22 08:44:50 +03:00
yggverse
c79f386bf1 update version 2025-03-28 00:33:33 +02:00
yggverse
9bbaecf344 make Size struct for tuple argument, move Size to separated mod 2025-03-27 21:33:50 +02:00
yggverse
4dddbd5f8a make Size struct for tuple argument 2025-03-27 21:14:14 +02:00
yggverse
b6ea830545 update example 2025-03-25 07:38:54 +02:00
yggverse
8ee088270f implement high-level getters, add comments, improve tests 2025-03-25 07:36:48 +02:00
yggverse
46da3a031a remove extras 2025-03-25 07:08:20 +02:00
yggverse
ea1fb8ea66 update temporary status codes api 2025-03-25 05:13:56 +02:00
yggverse
c9a59e76ee optimize tests format 2025-03-25 05:13:40 +02:00
yggverse
e96ff688b3 update permanent status codes api 2025-03-25 04:43:56 +02:00
yggverse
0c75da793f add missed tests members, enshort local var names 2025-03-25 03:26:39 +02:00
yggverse
064c4107f3 reduce local var names len 2025-03-25 03:21:59 +02:00
yggverse
d565d56c17 implement message_or_default method, add comments, add missed members test 2025-03-25 03:20:40 +02:00
yggverse
3b24625d66 implement message_or_default method, add comments 2025-03-25 03:01:19 +02:00
yggverse
f513747e86 add alias getters test 2025-03-25 02:21:03 +02:00
yggverse
5229cdae85 reorganize redirection structs format: make constructors lazy, parse members on get 2025-03-25 02:18:02 +02:00
yggverse
473ed48715 make final codes public, add comments 2025-03-24 23:32:18 +02:00
yggverse
4eb998ef20 draft potential test 2025-03-24 22:51:55 +02:00
yggverse
232531a0bc reorganize certificate structs format: make constructors lazy, parse members on get 2025-03-24 22:50:03 +02:00
yggverse
1b96270598 remove deprecated enum values 2025-03-24 22:38:07 +02:00
yggverse
845f3dc77e enshort var names 2025-03-24 22:36:00 +02:00
yggverse
e94923ecb5 fix last byte inclusion 2025-03-24 20:49:33 +02:00
yggverse
a32eccf5cb reorganize input format: make constructors lazy, parse members on get 2025-03-24 20:46:54 +02:00
yggverse
161142c809 rename mode const 2025-03-24 19:57:54 +02:00
yggverse
8feab6b93b rename constructors 2025-03-24 19:40:12 +02:00
yggverse
5360c6bf19 close code members 2025-03-24 18:31:55 +02:00
yggverse
68277f8e83 update example 2025-03-24 07:43:44 +02:00
yggverse
71043bbf73 remove extra format 2025-03-24 07:38:26 +02:00
yggverse
3de096cced add tests 2025-03-24 07:35:57 +02:00
yggverse
68e7894125 apply clippy 2025-03-24 07:10:22 +02:00
yggverse
0717e473b7 remove unsupported modes, add comments 2025-03-24 07:06:54 +02:00
yggverse
7c518cecf6 begin header holder implementation with lazy parser by getters, add request::Mode, add common header_bytes helper 2025-03-24 06:50:08 +02:00
yggverse
a12a73d311 hold NetworkAddress and SocketConnection as the Connection members 2025-03-22 19:03:42 +02:00
yggverse
2102d8887a fix codes 2025-03-19 15:07:22 +02:00
yggverse
9eb21bb6a3 Revert "hold raw header string"
This reverts commit 5bb52fbd8c.
2025-03-19 15:06:53 +02:00
yggverse
3f968d87b1 update error enum 2025-03-19 03:25:55 +02:00
yggverse
ab8eb402a8 decode header bytes only 2025-03-19 03:23:20 +02:00
yggverse
6dbf49cea3 validate header len 2025-03-19 03:13:37 +02:00
yggverse
b62f990bf2 fix codes, validate header len 2025-03-19 03:12:43 +02:00
yggverse
376473660f update minor version 2025-03-19 01:17:14 +02:00
yggverse
5bb52fbd8c hold raw header string 2025-03-19 01:16:51 +02:00
yggverse
af8a972cca update version 2025-03-18 00:48:58 +02:00
yggverse
0aeb501760 apply new version requirements 2025-03-16 22:01:40 +02:00
yggverse
e6661c1d00 apply new fmt version 2025-03-16 21:58:00 +02:00
yggverse
fc8356f7ac update rust version 2025-03-16 21:31:22 +02:00
yggverse
90cc58ab92 update version 2025-03-15 17:11:44 +02:00
yggverse
d7166dac66 update tests 2025-03-15 14:49:30 +02:00
yggverse
06fc69cff8 update dependencies version 2025-03-15 14:44:30 +02:00
yggverse
0523f67850 add support for uri starts with double slash 2025-03-15 13:16:14 +02:00
yggverse
e3abd89c9d update version 2025-02-24 07:52:43 +02:00
yggverse
564f5b69d5 update version 2025-02-24 07:50:49 +02:00
yggverse
1ff38ee838 fix maximum payload of 16 kB by using write_all method, hold bytes on request error 2025-02-24 07:49:41 +02:00
yggverse
4f6799a495 remove extra clone 2025-02-24 07:38:09 +02:00
yggverse
f51c636401 update version 2025-02-16 23:11:17 +02:00
yggverse
e635c41065 add funding info 2025-02-15 21:11:41 +02:00
yggverse
862ab1ccfa increase default timeout to 30 seconds 2025-02-13 07:14:41 +02:00
yggverse
9ce509cedc update readme 2025-02-10 02:02:45 +02:00
yggverse
8334d8a83c update comment 2025-02-10 02:00:54 +02:00
yggverse
0f1caadc03 update comment 2025-02-10 01:57:35 +02:00
yggverse
83b29c0276 use common callback 2025-02-10 00:17:08 +02:00
yggverse
582744f830 update comments 2025-02-09 02:50:34 +02:00
yggverse
bb8c2273d4 remove unspecified condition, skip handle the chunk on zero bytes received 2025-02-09 02:02:55 +02:00
yggverse
62f53304aa stop chunk iteration on match len < chunk condition (some servers may close the connection immediately); hold memory_input_stream in the error returned 2025-02-09 00:42:57 +02:00
yggverse
d4f076f074 update version 2025-02-03 14:04:49 +02:00
yggverse
b3e3f2e07b rollback release version 2025-02-03 14:02:19 +02:00
yggverse
a953601141 update version 2025-02-03 13:59:40 +02:00
yggverse
867945ec74 rename method, apply minor optimizations 2025-02-03 13:55:31 +02:00
yggverse
46483d1829 rename methods, change arguments order 2025-02-03 13:48:57 +02:00
yggverse
4ee92645ca remove deprecated features 2025-02-03 13:38:34 +02:00
yggverse
1505b6311c rename arguments, use tuple for arguments group 2025-02-03 13:38:04 +02:00
yggverse
c9d5e5987c minor optimizations 2025-02-03 13:05:37 +02:00
yggverse
998a4e97b4 fix uri arguments 2025-02-03 11:27:10 +02:00
yggverse
041454d8df update example 2025-02-03 03:10:25 +02:00
yggverse
9bb926f243 update example 2025-02-03 03:03:36 +02:00
yggverse
d57d9fc7df update enum name 2025-02-03 02:55:42 +02:00
yggverse
7518101b55 implement to_uri method 2025-02-03 02:44:33 +02:00
yggverse
517153656b update example 2025-02-03 02:09:41 +02:00
yggverse
dc2300b1c0 use human-readable bytes format 2025-02-03 01:47:50 +02:00
yggverse
a5fbca2ace fix route by first byte 2025-02-03 01:20:08 +02:00
yggverse
788b792167 return connection in result 2025-02-03 01:08:31 +02:00
yggverse
8df7af44b5 exclude message from string trait 2025-02-02 23:08:42 +02:00
yggverse
5358e43697 update Response API 2025-02-02 22:12:40 +02:00
yggverse
cdac038135 fix example 2025-01-28 11:58:35 +02:00
yggverse
eee87d66b4 update version 2025-01-28 10:19:24 +02:00
yggverse
016f82d586 remove deprecated namespace 2025-01-27 21:09:02 +02:00
yggverse
6da4c2ed52 implement titan and gemini requests in single file 2025-01-27 20:42:17 +02:00
yggverse
0cb5ff9cbc update example 2025-01-26 08:28:14 +02:00
yggverse
f669dc6b23 remove extras 2025-01-25 23:41:28 +02:00
yggverse
86af47ff49 add DEFAULT_MIME const with comments 2025-01-25 22:16:47 +02:00
yggverse
aa44e2723d update readme 2025-01-25 21:29:41 +02:00
yggverse
67989dba63 implement local tests 2025-01-25 21:27:42 +02:00
yggverse
37d30d700c define size inline 2025-01-25 20:51:16 +02:00
yggverse
ee0216a1a0 handle Titan requests 2025-01-22 18:35:10 +02:00
yggverse
caa61bb808 implement Uri reference getter 2025-01-22 16:27:28 +02:00
yggverse
ea9d7e4c5d update example 2025-01-22 15:15:54 +02:00
yggverse
075b5605a0 rename constructor 2025-01-22 15:15:44 +02:00
yggverse
5e52e74870 update request api 2025-01-22 15:14:59 +02:00
yggverse
52141f3dca remove extra functions 2025-01-22 10:13:15 +02:00
yggverse
490a513ddf update member data type 2025-01-19 04:24:43 +02:00
yggverse
e96771b926 add as_gstring method 2025-01-19 03:52:46 +02:00
yggverse
48c7676788 change Text structure, implement Display trait 2025-01-19 03:29:22 +02:00
yggverse
946ff485be store value as GString, implement as_gstring, to_gstring methods 2025-01-18 03:37:26 +02:00
yggverse
cac544043e add as_str getter 2025-01-18 03:26:11 +02:00
yggverse
90bff09269 rename variable 2025-01-18 02:55:49 +02:00
yggverse
d999e64e02 rename variable 2025-01-18 02:55:06 +02:00
yggverse
b5be9dcc76 remove extra conversions 2025-01-18 02:53:33 +02:00
yggverse
5dd78dd43d change Data struct, replace GString with String, implement Display trait 2025-01-18 02:51:06 +02:00
yggverse
962558c123 reorder implementations 2025-01-18 02:40:21 +02:00
yggverse
edf9982933 remove extra conversion 2025-01-18 02:38:32 +02:00
yggverse
c5aada49b4 remove extra conversions 2025-01-18 02:36:08 +02:00
yggverse
b5e864e807 update comment 2025-01-18 02:27:23 +02:00
yggverse
e456719e58 store MIME value in lowercase presentation 2025-01-18 02:18:56 +02:00
yggverse
d3133f50f7 change Mime struct, implement Display trait 2025-01-18 02:08:19 +02:00
yggverse
09b2c626a3 implement Display trait 2025-01-17 22:26:19 +02:00
yggverse
61fbab6dae use FnOnce for callback function 2025-01-17 07:32:09 +02:00
yggverse
c67593e5fd update gemini test conditions 2025-01-14 00:47:53 +02:00
yggverse
339f0bb1af change arg attribute name 2025-01-14 00:44:12 +02:00
yggverse
ac53f73a60 implement titan test 2025-01-14 00:42:59 +02:00
yggverse
df191c8e25 add query support 2025-01-14 00:42:40 +02:00
yggverse
a9283770db implement to_bytes method 2025-01-14 00:42:17 +02:00
yggverse
f4c9b73925 make MIME type argument optional 2025-01-13 23:41:12 +02:00
yggverse
ce5d3ac4d2 implement Request constructors, remove build methods 2025-01-13 23:14:54 +02:00
yggverse
e2097138a9 add requirements installation guide 2025-01-13 22:46:43 +02:00
yggverse
eab1786918 remove extra size argument 2025-01-13 22:17:01 +02:00
yggverse
1c0de40617 remove extra variable 2025-01-13 22:10:35 +02:00
yggverse
042e2a16ea remove extra conditions 2025-01-13 22:10:00 +02:00
yggverse
fa02234cbd implement default_port option for to_network_address method 2025-01-13 22:05:53 +02:00
yggverse
a2261601f6 update comment 2025-01-13 21:57:41 +02:00
yggverse
f93fd035ef use Self alias 2025-01-13 21:56:06 +02:00
yggverse
8492ea7db0 update comment 2025-01-13 21:54:55 +02:00
yggverse
fecbbff18f implement to_network_address method 2025-01-13 21:52:12 +02:00
yggverse
007921f73f add libglib2.0-dev dependency 2025-01-13 21:30:19 +02:00
yggverse
fbdc20fd13 update readme 2025-01-13 21:28:00 +02:00
yggverse
74e5e4d976 update readme 2025-01-13 21:25:05 +02:00
yggverse
54f2b81475 add versions debug API 2025-01-13 21:23:52 +02:00
yggverse
29b835411d implement Titan protocol features 2025-01-13 21:22:03 +02:00
yggverse
66a0de6a8e update readme 2024-12-21 20:28:40 +02:00
yggverse
e2bee95140 add badge 2024-12-20 10:22:50 +02:00
yggverse
256939c3b4 rename workflow 2024-12-20 10:22:35 +02:00
yggverse
87ccca5373 update version 2024-12-14 10:29:46 +02:00
yggverse
54fabd241b update example with new api version 2024-12-14 06:28:48 +02:00
yggverse
7e9c574d4a remove extra line 2024-12-14 06:23:10 +02:00
yggverse
bdc2b50940 remove mime type enumeration as external feature, parse and return raw string instead 2024-12-12 11:58:18 +02:00
yggverse
249199f780 change api version 2024-12-12 10:01:10 +02:00
yggverse
2c88f12f2d dump unsupported mime type string 2024-12-12 10:00:41 +02:00
yggverse
76235da239 update version 2024-12-11 10:24:08 +02:00
yggverse
e76bc62d82 add mime type groups support 2024-12-11 10:19:14 +02:00
yggverse
1911b0ad95 return bytes total on_complete 2024-12-09 14:50:31 +02:00
yggverse
f33e53e51b remove extra tuple 2024-12-09 14:33:51 +02:00
yggverse
eb32db3d3b make bytes_total_limit optional 2024-12-09 14:14:01 +02:00
yggverse
8c298977f3 update version 2024-12-09 14:01:45 +02:00
yggverse
55ee734a0b add FileOutputStream API 2024-12-09 14:01:19 +02:00
yggverse
059fa8f2d7 rename method from read_all_from_stream_async to move_all_from_stream_async, change arguments order, update comments 2024-12-09 14:00:35 +02:00
yggverse
6ac26bad62 validate warnings 2024-12-07 22:52:12 +02:00
yggverse
1f05ccc149 add rustfmt, clippy validation 2024-12-04 05:56:07 +02:00
yggverse
18806e2f38 update comments 2024-12-03 20:15:07 +02:00
yggverse
f0f34dfdb2 update error message 2024-12-02 13:50:21 +02:00
yggverse
128b5d68b6 change version 2024-12-01 13:50:44 +02:00
yggverse
3c627d6e4b rollback to release version 2024-12-01 13:49:55 +02:00
yggverse
7802ffad67 change version 2024-12-01 13:47:54 +02:00
yggverse
a77e4abf50 add usage example 2024-12-01 13:43:07 +02:00
yggverse
d313f900ba update comment 2024-12-01 13:12:07 +02:00
yggverse
41d7d8e4f3 update comment 2024-12-01 13:09:22 +02:00
yggverse
3fbd6ff3e3 update comment 2024-12-01 13:02:34 +02:00
yggverse
ce19e94db9 add comment 2024-12-01 13:01:30 +02:00
yggverse
94d63bd6de make session resumption optional 2024-12-01 12:57:54 +02:00
yggverse
096bd1d862 rename integration file 2024-12-01 11:51:10 +02:00
d47081
11313eafb9
Create rust.yml 2024-12-01 11:49:22 +02:00
yggverse
66245ef8dc update comment 2024-12-01 11:05:22 +02:00
yggverse
e9d51697bd remove extra comment 2024-12-01 10:52:25 +02:00
yggverse
4c0fea0e99 remove deprecated event listener 2024-12-01 10:50:25 +02:00
yggverse
1a8bd44841 update comments 2024-12-01 10:44:04 +02:00
yggverse
193dbef087 return plain IOStream object 2024-12-01 10:41:01 +02:00
yggverse
3791cbc4d0 fix guest tls connection init 2024-12-01 10:37:05 +02:00
yggverse
a63e05685c update comment 2024-12-01 09:29:26 +02:00
yggverse
8947052718 make new_tls_client_connection public, update comments 2024-12-01 09:23:23 +02:00
yggverse
e442a2880a fix guest certificate session cast 2024-12-01 09:15:53 +02:00
yggverse
8b6f2200f5 update comment 2024-12-01 08:51:59 +02:00
yggverse
4767929050 update response namespace 2024-12-01 08:50:28 +02:00
yggverse
70fc128c29 drop extra move 2024-12-01 08:10:12 +02:00
yggverse
730af453f6 remove extra clone 2024-12-01 07:04:21 +02:00
yggverse
7d90d974a0 drop extra move 2024-12-01 06:51:41 +02:00
yggverse
6330bbfd85 rename method, update comments 2024-12-01 04:49:10 +02:00
yggverse
8f910672e2 require Priority, Cancellable arguments, remove extra members 2024-12-01 04:35:19 +02:00
yggverse
2df9f36599 fix cancellation construction 2024-12-01 03:59:36 +02:00
yggverse
99583aa719 remove extra reference 2024-12-01 03:58:13 +02:00
yggverse
4038172735 rename variable to socket_connection 2024-12-01 03:47:20 +02:00
yggverse
0cc6d10017 remove rc wrap 2024-12-01 03:21:43 +02:00
yggverse
98c6150f74 update comments 2024-12-01 03:11:40 +02:00
yggverse
17b2fcaaae add comments 2024-12-01 03:05:46 +02:00
yggverse
273dac139e remove extra certificate holder 2024-12-01 03:04:01 +02:00
yggverse
3cc9fcd86b replace deprecated re-handshake feature with session-resumption-enabled property set 2024-12-01 02:57:21 +02:00
yggverse
911fb13a69 update comment 2024-11-30 19:22:14 +02:00
yggverse
fe22b84916 update comment 2024-11-30 19:14:30 +02:00
yggverse
af4a55659b remove deprecated notice 2024-11-30 18:59:57 +02:00
yggverse
efc9b62786 use Cancellable::NONE for re-handshake action 2024-11-30 18:59:06 +02:00
yggverse
cca3e4daa6 cancel and close previous client sessions 2024-11-30 18:52:59 +02:00
yggverse
072e6c3e7c update comments 2024-11-30 18:33:41 +02:00
yggverse
5e9e3aecd2 update comment 2024-11-30 18:27:25 +02:00
yggverse
2157e55db9 update comment 2024-11-30 18:25:34 +02:00
yggverse
ec34228e0f update comment 2024-11-30 18:24:50 +02:00
yggverse
5b01a5af54 update comment 2024-11-30 18:20:04 +02:00
yggverse
c61568a5d8 update comments 2024-11-30 18:19:04 +02:00
yggverse
0f8b98c395 update all sessions match certificate scope 2024-11-30 18:16:46 +02:00
yggverse
c12c57cc99 add connection cancel request 2024-11-30 18:00:38 +02:00
yggverse
559e03f904 handle cancellable option 2024-11-30 17:38:03 +02:00
yggverse
cdf35db0d6 implement cancel action 2024-11-30 17:26:03 +02:00
yggverse
6ee60e9d9d handle SocketConnection close errors, remove deprecated implementation 2024-11-30 16:55:43 +02:00
yggverse
79f219ba76 disable TlsClientConnection close to prevent rehandshake failure on user certificate change in runtime 2024-11-30 16:45:02 +02:00
yggverse
fbfd7a2c67 update comment 2024-11-30 05:38:46 +02:00
yggverse
a715e7632a update comment 2024-11-30 05:38:14 +02:00
yggverse
36569da73b update comment 2024-11-30 05:32:49 +02:00
yggverse
0ab6f97815 update comment 2024-11-30 05:31:48 +02:00
yggverse
e86a556863 update comment 2024-11-30 05:28:41 +02:00
yggverse
2e6cdb000b update comment 2024-11-30 05:27:16 +02:00
yggverse
ed68c22010 add comment 2024-11-30 05:26:14 +02:00
yggverse
c779ca3788 add shared cancellable holder 2024-11-30 05:08:45 +02:00
yggverse
1dfaf68267 rename enum option 2024-11-30 04:35:02 +02:00
yggverse
653960c1ab update comment 2024-11-30 04:32:51 +02:00
yggverse
8618b31570 remove extra construction 2024-11-30 04:24:47 +02:00
yggverse
9e3176ca00 update comment 2024-11-30 04:18:31 +02:00
yggverse
873489df29 reorder actions, add comments 2024-11-30 04:17:27 +02:00
yggverse
a06e4e9eff enshort common error name 2024-11-30 04:05:31 +02:00
yggverse
809e54b887 update comment 2024-11-30 03:44:14 +02:00
yggverse
c57ac0de83 update comment 2024-11-30 03:42:28 +02:00
yggverse
16971f9321 update comment 2024-11-30 03:40:53 +02:00
yggverse
911539eab9 update comment 2024-11-30 03:39:57 +02:00
yggverse
71135fac58 add comment 2024-11-30 03:39:18 +02:00
yggverse
745b8a6786 update comment 2024-11-30 03:35:00 +02:00
yggverse
9f9e0e0ea3 update comment 2024-11-30 03:32:37 +02:00
yggverse
d0a26f429b reorder namespaces 2024-11-30 03:17:08 +02:00
yggverse
a0fa799163 update comment 2024-11-30 03:16:40 +02:00
yggverse
ab9c7f4400 move session update method to struct implementation 2024-11-30 03:15:57 +02:00
yggverse
2db7d77f43 replace the closure with the function itself 2024-11-30 02:01:15 +02:00
yggverse
7db362aadc add default implementation 2024-11-30 01:59:40 +02:00
yggverse
c3a76472d2 update session update detection 2024-11-30 01:49:50 +02:00
yggverse
b3e9bf239c add tls_client_connection, rehandshake methods 2024-11-30 01:48:33 +02:00
yggverse
c4c173f6cf implement session update on certificate change in runtime 2024-11-29 17:54:37 +02:00
yggverse
f4cb0c3bcc handle errors 2024-11-28 23:27:53 +02:00
yggverse
16ed3efef0 implement close method, add comments 2024-11-28 21:40:21 +02:00
yggverse
1c50fadfde get connection ownership on wrap 2024-11-28 02:31:50 +02:00
yggverse
e437d35acf use native TlsCertificate 2024-11-28 02:21:15 +02:00
yggverse
2079bb1167 rename methods 2024-11-28 01:58:17 +02:00
yggverse
9ebd1e03f6 use certificate wrapper 2024-11-27 22:54:18 +02:00
yggverse
6c88eedd33 update comment 2024-11-27 22:23:55 +02:00
yggverse
2e7df281a9 update comment 2024-11-27 22:20:30 +02:00
yggverse
1fbcfcfff0 update comment 2024-11-27 20:06:44 +02:00
yggverse
b1b35e059b add guest certificates validation 2024-11-27 19:56:36 +02:00
yggverse
c61164e666 toggle set_tls for guest sessions 2024-11-27 18:52:39 +02:00
yggverse
8f2820b171 require v2_70 2024-11-27 18:33:34 +02:00
yggverse
5e20e5a925 update comments 2024-11-27 18:25:03 +02:00
yggverse
b9be89ca0f set DEFAULT_PORT cost as global 2024-11-27 18:21:16 +02:00
yggverse
ffda3d27e3 add to_network_address method 2024-11-27 18:17:25 +02:00
yggverse
4e712260ff add client certificate api 2024-11-27 18:03:22 +02:00
yggverse
9d240c4c37 fix comments 2024-11-27 16:48:22 +02:00
yggverse
c7f992e1b3 implement errors handler 2024-11-27 16:46:34 +02:00
yggverse
cc0625d920 update argument types 2024-11-27 16:45:39 +02:00
yggverse
cbaa4c0e81 fix enum name 2024-11-27 16:11:54 +02:00
yggverse
017a63b4e5 update enum names 2024-11-27 16:11:30 +02:00
yggverse
5737b89278 delegate from_uri method to gio::network_address wrapper 2024-11-27 15:55:39 +02:00
yggverse
239786da6a update memory input stream errors handler 2024-11-27 15:39:37 +02:00
yggverse
3a9e84a3d9 draft new api version 2024-11-27 05:50:09 +02:00
yggverse
67d486cc4d remove gio 2.70+ requirement 2024-11-16 09:34:37 +02:00
yggverse
32a879b756 require gio 2.70+ 2024-11-16 09:18:25 +02:00
yggverse
af7aaa63e4 add Default implementation for Text 2024-11-15 22:20:02 +02:00
yggverse
1d4dc50380 apply some clippy corrections 2024-11-15 22:01:47 +02:00
yggverse
d6dc4d6870 replace SocketConnection with IOStream implementation (for future client certificate support) 2024-11-15 21:46:52 +02:00
yggverse
c1c06667c9 fix undefined mime type detection 2024-11-03 00:11:05 +02:00
yggverse
7215258a71 disable unstable feature 2024-11-02 22:45:06 +02:00
yggverse
03b2c36153 update readme 2024-11-01 19:54:25 +02:00
yggverse
fab352e0b5 update readme 2024-11-01 19:31:15 +02:00
yggverse
5aaea57304 update readme 2024-11-01 19:28:33 +02:00
yggverse
6435a88cb6 update readme 2024-11-01 19:27:46 +02:00
yggverse
6e46ddb676 update readme 2024-11-01 19:26:37 +02:00
yggverse
cb56e614ae update readme 2024-11-01 19:23:14 +02:00
yggverse
cd9b132952 update readme 2024-11-01 19:20:44 +02:00
yggverse
cac4168645 update readme 2024-11-01 19:19:33 +02:00
yggverse
c4fa868345 add documentation comments 2024-11-01 19:12:19 +02:00
yggverse
9dd1bcc9dd implement all status codes support 2024-11-01 19:07:26 +02:00
yggverse
d34d24588f hold code in status enum 2024-11-01 18:54:19 +02:00
yggverse
ac17a48144 add svg image format 2024-11-01 17:51:52 +02:00
yggverse
b6134c92d5 update version 2024-11-01 05:37:00 +02:00
yggverse
85ea1ef0b2 update readme 2024-11-01 05:34:37 +02:00
yggverse
0d0ce49d98 update description 2024-11-01 05:33:30 +02:00
yggverse
5e315c0259 make MAX_LEN local 2024-11-01 05:30:40 +02:00
yggverse
1b1ab6bea0 update comment 2024-11-01 05:29:45 +02:00
yggverse
748ccabebb update comments 2024-11-01 05:28:36 +02:00
yggverse
3f6688b824 update comments 2024-11-01 05:22:14 +02:00
yggverse
10205f3147 make data optional on empty 2024-11-01 05:21:47 +02:00
yggverse
a0827e0425 update comments 2024-11-01 05:09:35 +02:00
yggverse
7d837c552b update comments 2024-11-01 05:05:41 +02:00
yggverse
fc48a08be9 add comments 2024-11-01 05:02:56 +02:00
yggverse
8bfea4ffcf make mime type optional 2024-11-01 04:32:53 +02:00
yggverse
5c0b8bf386 separate constructors 2024-10-31 05:32:10 +02:00
yggverse
97b0317509 update version 2024-10-31 05:29:07 +02:00
yggverse
20270d6b30 move body component to data::text namespace 2024-10-31 05:17:02 +02:00
yggverse
d5cea24891 draft new components 2024-10-31 03:24:41 +02:00
yggverse
394ae0cf4f update comment 2024-10-31 03:22:27 +02:00
yggverse
b73a9388ee update comment 2024-10-31 03:17:57 +02:00
yggverse
63a9874dcf add comment 2024-10-31 03:14:52 +02:00
yggverse
06345dedaf add comment 2024-10-31 03:11:08 +02:00
yggverse
4188842eb5 update comment 2024-10-31 02:39:22 +02:00
yggverse
c1bae6e6b5 add comment 2024-10-31 02:31:10 +02:00
yggverse
99d7baff58 update version 2024-10-31 01:00:04 +02:00
yggverse
53473c38fe update comments 2024-10-31 00:59:51 +02:00
yggverse
c9209b1743 remove extra conversions 2024-10-30 23:52:25 +02:00
yggverse
93985095a5 update api version with new implementation 2024-10-30 18:28:20 +02:00
yggverse
47f58f800d add initial capacity 2024-10-30 14:23:40 +02:00
yggverse
36b8342f29 update meta parser 2024-10-30 14:22:08 +02:00
yggverse
8c1afd654a update meta parser 2024-10-30 05:41:25 +02:00
yggverse
f8fa101c59 update comment 2024-10-30 04:50:27 +02:00
yggverse
daa84b6a6b update comment 2024-10-30 04:49:50 +02:00
yggverse
ebfdf6a1d0 update comment 2024-10-30 04:47:59 +02:00
yggverse
2331bf8e27 fix typo 2024-10-30 04:40:20 +02:00
yggverse
8dcab0f29e add new enums 2024-10-30 04:37:05 +02:00
yggverse
c2b06fd688 fix status code error types 2024-10-30 04:35:04 +02:00
yggverse
8f7bbaec76 add Protocol error 2024-10-30 04:31:26 +02:00
yggverse
268dab6ed3 use clone of shared reference instead of ref 2024-10-29 21:19:49 +02:00
yggverse
028ffa384d derive debug 2024-10-29 17:06:48 +02:00
yggverse
3e8da1725c remove option wrapper from callbacks 2024-10-29 16:51:05 +02:00
yggverse
4ab83702e9 use reference without copy usize 2024-10-29 16:33:32 +02:00
yggverse
02ced24cce implement gio::memory_input_stream 2024-10-29 16:01:05 +02:00
yggverse
0bbcb6372d reorder asc 2024-10-28 20:38:27 +02:00
yggverse
64cc68e9bd add audio formats 2024-10-28 20:36:37 +02:00
yggverse
338d0b7b6f add parsing category 2024-10-28 15:22:32 +02:00
yggverse
8e516b534d fix redirect status detection 2024-10-28 14:25:22 +02:00
yggverse
a26e59d642 update readme 2024-10-28 13:48:25 +02:00
yggverse
7b0bc23a9f add HEADER_BYTES_LEN 2024-10-28 13:40:16 +02:00
yggverse
2a01232f6a rename entities 2024-10-28 03:07:12 +02:00
yggverse
b2bceb775a update error enum 2024-10-28 02:55:25 +02:00
yggverse
5a30b63a27 update readme 2024-10-28 02:41:42 +02:00
yggverse
c413b8239e update readme 2024-10-28 02:40:54 +02:00
yggverse
e7f4d11aae update readme 2024-10-28 02:39:45 +02:00
yggverse
6514eb5919 update readme 2024-10-28 02:39:06 +02:00
yggverse
42441ebb24 update readme 2024-10-28 02:37:34 +02:00
yggverse
71d3de47d5 update readme 2024-10-28 02:36:43 +02:00
yggverse
0404ac4642 update readme 2024-10-28 02:33:56 +02:00
yggverse
f2da8044b1 update readme 2024-10-28 02:32:41 +02:00
yggverse
e71d531e46 update readme 2024-10-28 02:31:19 +02:00
yggverse
d707143cab update readme 2024-10-28 02:29:19 +02:00
yggverse
9152528790 draft new api version 2024-10-28 02:27:30 +02:00
yggverse
dccff1e111 update readme 2024-10-26 23:43:14 +03:00
yggverse
f83c5babb6 update readme 2024-10-26 23:42:21 +03:00
yggverse
2585b6ad4a update readme 2024-10-26 23:41:32 +03:00
yggverse
2c175cf684 update readme 2024-10-26 23:34:03 +03:00
yggverse
99fd5f42c0 update readme 2024-10-26 23:33:21 +03:00
yggverse
3459af295c update readme 2024-10-26 23:32:19 +03:00
yggverse
751d985f9b update readme 2024-10-26 23:30:11 +03:00
yggverse
ff5b1619c3 update readme 2024-10-26 23:28:30 +03:00
yggverse
ed038fd0af update readme 2024-10-26 23:27:24 +03:00
yggverse
81c1e011e0 update version 2024-10-26 23:25:36 +03:00
yggverse
ffb068e398 update readme 2024-10-26 23:25:30 +03:00
yggverse
3cde80b6a8 draft new api version 2024-10-26 23:22:26 +03:00
yggverse
8a5f1e2a57 return self anyway 2024-10-25 21:01:06 +03:00
yggverse
32c238da4d return entire self taken 2024-10-25 20:55:30 +03:00
yggverse
25bcb58fc4 update comment, change reference method 2024-10-25 20:53:57 +03:00
yggverse
7cb817a173 update readme 2024-10-25 19:34:11 +03:00
yggverse
1f25565a79 update readme 2024-10-25 19:31:59 +03:00
yggverse
6819de5276 update readme 2024-10-25 19:30:51 +03:00
yggverse
4bcbf50163 update readme 2024-10-25 19:30:19 +03:00
yggverse
60999bfa97 update readme 2024-10-25 16:45:00 +03:00
yggverse
16af9499b5 rename method 2024-10-25 16:34:10 +03:00
yggverse
bfc52606c3 update readme 2024-10-25 16:21:56 +03:00
yggverse
c9c8b39513 update variable name 2024-10-25 16:19:11 +03:00
yggverse
64eb173319 update readme 2024-10-25 16:18:19 +03:00
yggverse
62bbb1a925 update readme 2024-10-25 16:17:53 +03:00
yggverse
51685f18b7 fix anchors 2024-10-25 16:17:02 +03:00
yggverse
ff42d022a4 fix anchors 2024-10-25 16:16:02 +03:00
yggverse
052003b6be update readme 2024-10-25 16:14:53 +03:00
yggverse
afc030420b draft test 2024-10-25 16:13:57 +03:00
yggverse
fd96406dae remove extra line 2024-10-25 15:46:23 +03:00
yggverse
a8230bed37 update api 2024-10-25 15:01:17 +03:00
yggverse
9793a67187 draft request high-level api 2024-10-25 03:20:23 +03:00
yggverse
015c3ad7ca update readme 2024-10-24 20:46:35 +03:00
yggverse
f73ef32547 change version 2024-10-24 20:23:06 +03:00
yggverse
496e981138 delegate buffer features to input level, delete byte_buffer, input_stream mods as deprecated 2024-10-24 20:22:44 +03:00
yggverse
ee476e56d2 change version 2024-10-24 15:40:46 +03:00
yggverse
62c0f5ea83 add new constructors, add read_input_stream_async method, change read_input_stream api 2024-10-24 15:36:47 +03:00
yggverse
e8381892ef change version 2024-10-23 12:49:33 +03:00
yggverse
00a91d7c0c make status optional 2024-10-23 11:01:40 +03:00
yggverse
605c98e553 reorder asc 2024-10-22 21:00:27 +03:00
92 changed files with 4117 additions and 441 deletions

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
custom: https://yggverse.github.io/#donate

31
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,31 @@
name: Build
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run rustfmt
run: cargo fmt --all -- --check
- name: Update packages index
run: sudo apt update
- name: Install system packages
run: sudo apt install -y libglib2.0-dev
- name: Run clippy
run: cargo clippy --all-targets
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

View file

@ -1,19 +1,20 @@
[package]
name = "ggemini"
version = "0.1.1"
edition = "2021"
version = "0.20.1"
edition = "2024"
license = "MIT"
readme = "README.md"
description = "Glib-oriented client for Gemini protocol"
keywords = ["gemini", "gemini-protocol", "gtk", "gio", "client"]
categories = ["development-tools", "network-programming"]
description = "Glib/Gio-oriented network API for Gemini protocol"
keywords = ["gemini", "titan", "glib", "gio", "client"]
categories = ["development-tools", "network-programming", "parsing"]
repository = "https://github.com/YGGverse/ggemini"
[dependencies.gio]
package = "gio"
version = "0.20.4"
version = "0.21.0"
features = ["v2_70"]
[dependencies.glib]
package = "glib"
version = "0.20.4"
version = "0.21.0"
features = ["v2_66"]

View file

@ -1,7 +1,77 @@
# ggemini
Glib-oriented client for [Gemini protocol](https://geminiprotocol.net/)
![Build](https://github.com/YGGverse/ggemini/actions/workflows/build.yml/badge.svg)
[![Documentation](https://docs.rs/ggemini/badge.svg)](https://docs.rs/ggemini)
[![crates.io](https://img.shields.io/crates/v/ggemini.svg)](https://crates.io/crates/ggemini)
Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/)
> [!IMPORTANT]
> Project in development!
>
>
GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications dependent of [glib](https://crates.io/crates/glib) / [gio](https://crates.io/crates/gio) (`2.66+`) backend.
## Requirements
<details>
<summary>Debian</summary>
<pre>
sudo apt install libglib2.0-dev</pre>
</details>
<details>
<summary>Fedora</summary>
<pre>
sudo dnf install glib2-devel</pre>
</details>
## Install
```
cargo add ggemini
```
## Usage
* [Documentation](https://docs.rs/ggemini/latest/ggemini/)
### Example
``` rust
use gio::*;
use glib::*;
use ggemini::client::{
connection::{request::{Mode, Request}, Response},
Client,
};
fn main() -> ExitCode {
Client::new().request_async(
Request::Gemini { // or `Request::Titan`
uri: Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(),
mode: Mode::HeaderOnly // handle content separately (based on MIME)
},
Priority::DEFAULT,
Cancellable::new(),
None, // optional auth `GTlsCertificate`
None, // optional TOFU `GTlsCertificate` array
|result| match result {
Ok((response, _connection)) => match response {
Response::Success(success) => match success.mime().unwrap().as_str() {
"text/gemini" => todo!(),
_ => todo!(),
},
_ => todo!(),
},
Err(_) => todo!(),
},
);
ExitCode::SUCCESS
}
```
## Other crates
* [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API

View file

@ -1,10 +1,115 @@
//! High-level client API to interact with Gemini Socket Server:
//! * https://geminiprotocol.net/docs/protocol-specification.gmi
pub mod connection;
pub mod error;
pub mod response;
pub mod socket;
pub use connection::{Connection, Request, Response};
pub use error::Error;
pub use response::Response;
pub use socket::Socket;
// @TODO
use gio::{Cancellable, SocketClient, SocketProtocol, TlsCertificate, prelude::SocketClientExt};
use glib::Priority;
// Defaults
pub const DEFAULT_TIMEOUT: u32 = 30;
pub const DEFAULT_SESSION_RESUMPTION: bool = false;
/// Main point where connect external crate
///
/// Provides high-level API for session-safe interaction with
/// [Gemini](https://geminiprotocol.net) socket server
pub struct Client {
is_session_resumption: bool,
pub socket: SocketClient,
}
impl Default for Client {
fn default() -> Self {
Self::new()
}
}
impl Client {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
// Init new socket
let socket = SocketClient::new();
// Setup initial configuration for Gemini Protocol
socket.set_protocol(SocketProtocol::Tcp);
socket.set_timeout(DEFAULT_TIMEOUT);
// Done
Self {
is_session_resumption: DEFAULT_SESSION_RESUMPTION,
socket,
}
}
// Actions
/// Make new async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html),
/// callback with new `Response`on success or `Error` on failure
/// * compatible with user (certificate) and guest (certificate-less) connection types
pub fn request_async(
&self,
request: Request,
priority: Priority,
cancellable: Cancellable,
client_certificate: Option<TlsCertificate>,
server_certificates: Option<Vec<TlsCertificate>>,
callback: impl FnOnce(Result<(Response, Connection), Error>) + 'static,
) {
// Begin new connection
// * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid
// [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication)
match request.to_network_address(crate::DEFAULT_PORT) {
Ok(network_address) => {
self.socket
.connect_async(&network_address.clone(), Some(&cancellable.clone()), {
let is_session_resumption = self.is_session_resumption;
move |result| match result {
Ok(socket_connection) => {
match Connection::build(
socket_connection.clone(),
network_address,
client_certificate,
server_certificates,
is_session_resumption,
) {
Ok(connection) => connection.clone().request_async(
request,
priority,
cancellable,
move |result| {
callback(match result {
Ok(response) => Ok(response),
Err(e) => Err(Error::Request(connection, e)),
})
},
),
Err(e) => {
callback(Err(Error::Connection(socket_connection, e)))
}
}
}
Err(e) => callback(Err(Error::Connect(network_address, e))),
}
})
}
Err(e) => callback(Err(Error::NetworkAddress(e))),
}
}
// Setters
/// Change glib-networking `session-resumption-enabled` property (`false` by default)
/// * [Gemini specification](https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates)
/// * [GnuTLS manual](https://www.gnutls.org/manual/html_node/Session-resumption.html)
pub fn set_session_resumption(&mut self, is_enabled: bool) {
self.is_session_resumption = is_enabled
}
}

View file

@ -1,3 +1,172 @@
pub mod input_stream;
pub mod error;
pub mod request;
pub mod response;
// @TODO
pub use error::Error;
pub use request::{Mode, Request};
pub use response::Response;
use gio::{
Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection,
prelude::{IOStreamExt, OutputStreamExtManual, TlsCertificateExt, TlsConnectionExt},
};
use glib::{
Bytes, Priority,
object::{Cast, ObjectExt},
};
#[derive(Debug, Clone)]
pub struct Connection {
pub network_address: NetworkAddress,
pub socket_connection: SocketConnection,
pub tls_client_connection: TlsClientConnection,
}
impl Connection {
// Constructors
/// Create new `Self`
pub fn build(
socket_connection: SocketConnection,
network_address: NetworkAddress,
client_certificate: Option<TlsCertificate>,
server_certificates: Option<Vec<TlsCertificate>>,
is_session_resumption: bool,
) -> Result<Self, Error> {
Ok(Self {
tls_client_connection: match new_tls_client_connection(
&socket_connection,
Some(&network_address),
server_certificates,
is_session_resumption,
) {
Ok(tls_client_connection) => {
if let Some(ref c) = client_certificate {
tls_client_connection.set_certificate(c);
}
tls_client_connection
}
Err(e) => return Err(e),
},
network_address,
socket_connection,
})
}
// Actions
/// Send new `Request` to `Self` connection using
/// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) or
/// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol
pub fn request_async(
self,
request: Request,
priority: Priority,
cancellable: Cancellable,
callback: impl FnOnce(Result<(Response, Self), Error>) + 'static,
) {
let output_stream = self.stream().output_stream();
// Make sure **all header bytes** sent to the destination
// > A partial write is performed with the size of a message block, which is 16kB
// > https://docs.openssl.org/3.0/man3/SSL_write/#notes
output_stream.clone().write_all_async(
Bytes::from_owned(request.header()),
priority,
Some(&cancellable.clone()),
move |result| match result {
Ok(_) => match request {
Request::Gemini { mode, .. } => match mode {
Mode::HeaderOnly => Response::header_from_connection_async(
self,
priority,
cancellable,
|result, connection| {
callback(match result {
Ok(response) => Ok((response, connection)),
Err(e) => Err(Error::Response(e)),
})
},
),
},
// Make sure **all data bytes** sent to the destination
// > A partial write is performed with the size of a message block, which is 16kB
// > https://docs.openssl.org/3.0/man3/SSL_write/#notes
Request::Titan { data, mode, .. } => output_stream.write_all_async(
data,
priority,
Some(&cancellable.clone()),
move |result| match result {
Ok(_) => match mode {
Mode::HeaderOnly => Response::header_from_connection_async(
self,
priority,
cancellable,
|result, connection| {
callback(match result {
Ok(response) => Ok((response, connection)),
Err(e) => Err(Error::Response(e)),
})
},
),
},
Err((b, e)) => callback(Err(Error::Request(b, e))),
},
),
},
Err((b, e)) => callback(Err(Error::Request(b, e))),
},
)
}
// Getters
/// Get [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
/// * compatible with user (certificate) and guest (certificate-less) connection type
/// * useful to keep `Connection` reference active in async I/O context
pub fn stream(&self) -> IOStream {
self.tls_client_connection.clone().upcast::<IOStream>()
// * also `base_io_stream` method available @TODO
}
}
// Tools
/// Setup new [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html)
/// wrapper for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
/// using `server_identity` as the [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication)
fn new_tls_client_connection(
socket_connection: &SocketConnection,
server_identity: Option<&NetworkAddress>,
server_certificates: Option<Vec<TlsCertificate>>,
is_session_resumption: bool,
) -> Result<TlsClientConnection, Error> {
match TlsClientConnection::new(socket_connection, server_identity) {
Ok(tls_client_connection) => {
// Prevent session resumption (certificate change ability in runtime)
tls_client_connection.set_property("session-resumption-enabled", is_session_resumption);
// Return `Err` on server connection mismatch following specification lines:
// > Gemini servers MUST use the TLS close_notify implementation to close the connection
// > A client SHOULD notify the user of such a case
// https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections
tls_client_connection.set_require_close_notify(true);
// [TOFU](https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation)
tls_client_connection.connect_accept_certificate(move |_, c, _| {
server_certificates
.as_ref()
.is_none_or(|server_certificates| {
for server_certificate in server_certificates {
if server_certificate.is_same(c) {
return true;
}
}
false
})
});
Ok(tls_client_connection)
}
Err(e) => Err(Error::TlsClientConnection(e)),
}
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Request(glib::Bytes, glib::Error),
Response(crate::client::connection::response::Error),
TlsClientConnection(glib::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Request(_, e) => {
write!(f, "Request error: {e}")
}
Self::Response(e) => {
write!(f, "Response error: {e}")
}
Self::TlsClientConnection(e) => {
write!(f, "TLS client connection error: {e}")
}
}
}
}

View file

@ -1,4 +0,0 @@
pub mod byte_buffer;
pub use byte_buffer::ByteBuffer;
// @TODO

View file

@ -1,86 +0,0 @@
pub mod error;
pub use error::Error;
use gio::{prelude::InputStreamExt, Cancellable, InputStream};
use glib::{object::IsA, Bytes};
pub const DEFAULT_CAPACITY: usize = 0x400;
pub const DEFAULT_CHUNK_SIZE: usize = 0x100;
pub const DEFAULT_MAX_SIZE: usize = 0xfffff;
pub struct ByteBuffer {
bytes: Vec<Bytes>,
}
impl ByteBuffer {
/// Create dynamically allocated bytes buffer from `gio::InputStream`
///
/// Options:
/// * `capacity` bytes request to reduce extra memory overwrites (1024 by default)
/// * `chunk_size` bytes limit to read per iter (256 by default)
/// * `max_size` bytes limit to prevent memory overflow (1M by default)
pub fn from_input_stream(
input_stream: &InputStream, // @TODO
cancellable: Option<&impl IsA<Cancellable>>,
capacity: Option<usize>,
chunk_size: Option<usize>,
max_size: Option<usize>,
) -> Result<Self, Error> {
// Create buffer with initial capacity
let mut buffer: Vec<Bytes> = Vec::with_capacity(match capacity {
Some(value) => value,
None => DEFAULT_CAPACITY,
});
// Disallow unlimited buffer, use defaults on None
let limit = match max_size {
Some(value) => value,
None => DEFAULT_MAX_SIZE,
};
loop {
// Check buffer size to prevent memory overflow
if buffer.len() > limit {
return Err(Error::Overflow);
}
// Continue bytes reading
match input_stream.read_bytes(
match chunk_size {
Some(value) => value,
None => DEFAULT_CHUNK_SIZE,
},
cancellable,
) {
Ok(bytes) => {
// No bytes were read, end of stream
if bytes.len() == 0 {
break;
}
// Save chunk to buffer
buffer.push(bytes);
}
Err(_) => return Err(Error::Stream),
};
}
// Done
Ok(Self { bytes: buffer })
}
/// Get link to bytes collected
pub fn bytes(&self) -> &Vec<Bytes> {
&self.bytes
}
/// Return a copy of the bytes in UTF-8
pub fn to_utf8(&self) -> Vec<u8> {
self.bytes
.iter()
.flat_map(|byte| byte.iter())
.cloned()
.collect()
}
}

View file

@ -1,4 +0,0 @@
pub enum Error {
Overflow,
Stream,
}

View file

@ -0,0 +1,122 @@
pub mod error;
pub mod mode;
pub use error::Error;
pub use mode::Mode;
// Local dependencies
use gio::NetworkAddress;
use glib::{Bytes, Uri, UriHideFlags};
/// Single `Request` implementation for different protocols
pub enum Request {
Gemini {
uri: Uri,
mode: Mode,
},
Titan {
uri: Uri,
data: Bytes,
/// MIME type is optional attribute by Titan protocol specification,
/// but server MAY reject the request without `mime` value provided.
mime: Option<String>,
token: Option<String>,
mode: Mode,
},
}
impl Request {
// Getters
/// Generate header string for `Self`
pub fn header(&self) -> String {
match self {
Self::Gemini { uri, .. } => format!("{uri}\r\n"),
Self::Titan {
uri,
data,
mime,
token,
..
} => {
let mut header = format!(
"{};size={}",
uri.to_string_partial(UriHideFlags::QUERY),
data.len()
);
if let Some(mime) = mime {
header.push_str(&format!(";mime={mime}"));
}
if let Some(token) = token {
header.push_str(&format!(";token={token}"));
}
if let Some(query) = uri.query() {
header.push_str(&format!("?{query}"));
}
header.push_str("\r\n");
header
}
}
}
/// Get reference to `Self` [Uri](https://docs.gtk.org/glib/struct.Uri.html)
pub fn uri(&self) -> &Uri {
match self {
Self::Gemini { uri, .. } => uri,
Self::Titan { uri, .. } => uri,
}
}
/// Get [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) for `Self`
pub fn to_network_address(&self, default_port: u16) -> Result<NetworkAddress, Error> {
match crate::gio::network_address::from_uri(self.uri(), default_port) {
Ok(network_address) => Ok(network_address),
Err(e) => Err(Error::NetworkAddress(e)),
}
}
}
#[test]
fn test_gemini_header() {
use glib::UriFlags;
const REQUEST: &str = "gemini://geminiprotocol.net/";
assert_eq!(
Request::Gemini {
uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap(),
mode: Mode::HeaderOnly
}
.header(),
format!("{REQUEST}\r\n")
);
}
#[test]
fn test_titan_header() {
use glib::UriFlags;
const DATA: &[u8] = &[1, 2, 3];
const MIME: &str = "plain/text";
const TOKEN: &str = "token";
assert_eq!(
Request::Titan {
uri: Uri::parse(
"titan://geminiprotocol.net/raw/path?key=value",
UriFlags::NONE
)
.unwrap(),
data: Bytes::from(DATA),
mime: Some(MIME.to_string()),
token: Some(TOKEN.to_string()),
mode: Mode::HeaderOnly
}
.header(),
format!(
"titan://geminiprotocol.net/raw/path;size={};mime={MIME};token={TOKEN}?key=value\r\n",
DATA.len(),
)
);
}

View file

@ -0,0 +1,16 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
NetworkAddress(crate::gio::network_address::error::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::NetworkAddress(e) => {
write!(f, "Network Address error: {e}")
}
}
}
}

View file

@ -0,0 +1,6 @@
/// Request modes
pub enum Mode {
/// Request header bytes only, process content bytes manually
/// * useful for manual content type handle: text, stream or large content loaded by chunks
HeaderOnly,
}

View file

@ -0,0 +1,146 @@
pub mod certificate;
pub mod error;
pub mod failure;
pub mod input;
pub mod redirect;
pub mod success;
pub use certificate::Certificate;
pub use error::{Error, HeaderBytesError};
pub use failure::Failure;
pub use input::Input;
pub use redirect::Redirect;
pub use success::Success;
use super::Connection;
use gio::{Cancellable, IOStream};
use glib::{Priority, object::IsA};
const HEADER_LEN: usize = 1024;
/// https://geminiprotocol.net/docs/protocol-specification.gmi#responses
pub enum Response {
Input(Input), // 1*
Success(Success), // 2*
Redirect(Redirect), // 3*
Failure(Failure), // 4*,5*
Certificate(Certificate), // 6*
}
impl Response {
/// Asynchronously create new `Self` for given `Connection`
pub fn header_from_connection_async(
connection: Connection,
priority: Priority,
cancellable: Cancellable,
callback: impl FnOnce(Result<Self, Error>, Connection) + 'static,
) {
header_from_stream_async(
Vec::with_capacity(HEADER_LEN),
connection.stream(),
cancellable,
priority,
|result| {
callback(
match result {
Ok(buffer) => match buffer.first() {
Some(b) => match b {
b'1' => match Input::from_utf8(&buffer) {
Ok(input) => Ok(Self::Input(input)),
Err(e) => Err(Error::Input(e)),
},
b'2' => match Success::from_utf8(&buffer) {
Ok(success) => Ok(Self::Success(success)),
Err(e) => Err(Error::Success(e)),
},
b'3' => match Redirect::from_utf8(&buffer) {
Ok(redirect) => Ok(Self::Redirect(redirect)),
Err(e) => Err(Error::Redirect(e)),
},
b'4' | b'5' => match Failure::from_utf8(&buffer) {
Ok(failure) => Ok(Self::Failure(failure)),
Err(e) => Err(Error::Failure(e)),
},
b'6' => match Certificate::from_utf8(&buffer) {
Ok(certificate) => Ok(Self::Certificate(certificate)),
Err(e) => Err(Error::Certificate(e)),
},
b => Err(Error::Code(*b)),
},
None => Err(Error::Protocol(buffer)),
},
Err(e) => Err(e),
},
connection,
)
},
)
}
}
// Tools
/// Asynchronously read header bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
///
/// Return UTF-8 buffer collected
/// * requires `IOStream` reference to keep `Connection` active in async thread
fn header_from_stream_async(
mut buffer: Vec<u8>,
stream: impl IsA<IOStream>,
cancellable: Cancellable,
priority: Priority,
callback: impl FnOnce(Result<Vec<u8>, Error>) + 'static,
) {
use gio::prelude::{IOStreamExt, InputStreamExtManual};
stream.input_stream().read_async(
vec![0],
priority,
Some(&cancellable.clone()),
move |result| match result {
Ok((bytes, size)) => {
if size == 0 {
return callback(Ok(buffer));
}
if buffer.len() + bytes.len() > HEADER_LEN {
buffer.extend(bytes);
return callback(Err(Error::Protocol(buffer)));
}
if bytes[0] == b'\r' {
buffer.extend(bytes);
return header_from_stream_async(
buffer,
stream,
cancellable,
priority,
callback,
);
}
if bytes[0] == b'\n' {
buffer.extend(bytes);
return callback(Ok(buffer));
}
buffer.extend(bytes);
header_from_stream_async(buffer, stream, cancellable, priority, callback)
}
Err((data, e)) => callback(Err(Error::Stream(e, data))),
},
)
}
/// Get header bytes slice
/// * common for all child parsers
fn header_bytes(buffer: &[u8]) -> Result<&[u8], HeaderBytesError> {
for (i, b) in buffer.iter().enumerate() {
if i > 1024 {
return Err(HeaderBytesError::Len);
}
if *b == b'\r' {
let n = i + 1;
if buffer.get(n).is_some_and(|b| *b == b'\n') {
return Ok(&buffer[..n + 1]);
}
break;
}
}
Err(HeaderBytesError::End)
}

View file

@ -0,0 +1,111 @@
pub mod error;
pub mod not_authorized;
pub mod not_valid;
pub mod required;
pub use error::Error;
pub use not_authorized::NotAuthorized;
pub use not_valid::NotValid;
pub use required::Required;
const CODE: u8 = b'6';
/// 6* status code group
/// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates
pub enum Certificate {
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60
Required(Required),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized
NotAuthorized(NotAuthorized),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid
NotValid(NotValid),
}
impl Certificate {
// Constructors
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
match buffer.first() {
Some(b) => match *b {
CODE => match buffer.get(1) {
Some(b) => match *b {
b'0' => Ok(Self::Required(
Required::from_utf8(buffer).map_err(Error::Required)?,
)),
b'1' => Ok(Self::NotAuthorized(
NotAuthorized::from_utf8(buffer).map_err(Error::NotAuthorized)?,
)),
b'2' => Ok(Self::NotValid(
NotValid::from_utf8(buffer).map_err(Error::NotValid)?,
)),
b => Err(Error::SecondByte(b)),
},
None => Err(Error::UndefinedSecondByte),
},
b => Err(Error::FirstByte(b)),
},
None => Err(Error::UndefinedFirstByte),
}
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty (not provided by server)
pub fn message(&self) -> Option<&str> {
match self {
Self::Required(required) => required.message(),
Self::NotAuthorized(not_authorized) => not_authorized.message(),
Self::NotValid(not_valid) => not_valid.message(),
}
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
match self {
Self::Required(required) => required.message_or_default(),
Self::NotAuthorized(not_authorized) => not_authorized.message_or_default(),
Self::NotValid(not_valid) => not_valid.message_or_default(),
}
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
match self {
Self::Required(required) => required.as_str(),
Self::NotAuthorized(not_authorized) => not_authorized.as_str(),
Self::NotValid(not_valid) => not_valid.as_str(),
}
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Required(required) => required.as_bytes(),
Self::NotAuthorized(not_authorized) => not_authorized.as_bytes(),
Self::NotValid(not_valid) => not_valid.as_bytes(),
}
}
}
#[test]
fn test() {
fn t(source: &str, message: Option<&str>) {
let b = source.as_bytes();
let c = Certificate::from_utf8(b).unwrap();
assert_eq!(c.message(), message);
assert_eq!(c.as_str(), source);
assert_eq!(c.as_bytes(), b);
}
// 60
t("60 Required\r\n", Some("Required"));
t("60\r\n", None);
// 61
t("61 Not Authorized\r\n", Some("Not Authorized"));
t("61\r\n", None);
// 62
t("61 Not Valid\r\n", Some("Not Valid"));
t("61\r\n", None);
}

View file

@ -0,0 +1,40 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
FirstByte(u8),
NotAuthorized(super::not_authorized::Error),
NotValid(super::not_valid::Error),
Required(super::required::Error),
SecondByte(u8),
UndefinedFirstByte,
UndefinedSecondByte,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::FirstByte(b) => {
write!(f, "Unexpected first byte: {b}")
}
Self::NotAuthorized(e) => {
write!(f, "NotAuthorized status parse error: {e}")
}
Self::NotValid(e) => {
write!(f, "NotValid status parse error: {e}")
}
Self::Required(e) => {
write!(f, "Required status parse error: {e}")
}
Self::SecondByte(b) => {
write!(f, "Unexpected second byte: {b}")
}
Self::UndefinedFirstByte => {
write!(f, "Undefined first byte")
}
Self::UndefinedSecondByte => {
write!(f, "Undefined second byte")
}
}
}
}

View file

@ -0,0 +1,79 @@
pub mod error;
pub use error::Error;
/// [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code
pub const CODE: &[u8] = b"61";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Certificate is not authorized";
/// Hold header `String` for [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct NotAuthorized(String);
impl NotAuthorized {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty (not provided by server)
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let na = NotAuthorized::from_utf8("61 Not Authorized\r\n".as_bytes()).unwrap();
assert_eq!(na.message(), Some("Not Authorized"));
assert_eq!(na.message_or_default(), "Not Authorized");
assert_eq!(na.as_str(), "61 Not Authorized\r\n");
assert_eq!(na.as_bytes(), "61 Not Authorized\r\n".as_bytes());
let na = NotAuthorized::from_utf8("61\r\n".as_bytes()).unwrap();
assert_eq!(na.message(), None);
assert_eq!(na.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(na.as_str(), "61\r\n");
assert_eq!(na.as_bytes(), "61\r\n".as_bytes());
// err
assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err());
assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err());
assert!(NotAuthorized::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(NotAuthorized::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,79 @@
pub mod error;
pub use error::Error;
/// [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code
pub const CODE: &[u8] = b"62";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Certificate is not valid";
/// Hold header `String` for [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct NotValid(String);
impl NotValid {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty (not provided by server)
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let nv = NotValid::from_utf8("62 Not Valid\r\n".as_bytes()).unwrap();
assert_eq!(nv.message(), Some("Not Valid"));
assert_eq!(nv.message_or_default(), "Not Valid");
assert_eq!(nv.as_str(), "62 Not Valid\r\n");
assert_eq!(nv.as_bytes(), "62 Not Valid\r\n".as_bytes());
let nv = NotValid::from_utf8("62\r\n".as_bytes()).unwrap();
assert_eq!(nv.message(), None);
assert_eq!(nv.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(nv.as_str(), "62\r\n");
assert_eq!(nv.as_bytes(), "62\r\n".as_bytes());
// err
// @TODO assert!(NotValid::from_utf8("62Fail\r\n".as_bytes()).is_err());
assert!(NotValid::from_utf8("63 Fail\r\n".as_bytes()).is_err());
assert!(NotValid::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(NotValid::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,79 @@
pub mod error;
pub use error::Error;
/// [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code
pub const CODE: &[u8] = b"60";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Certificate required";
/// Hold header `String` for [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct Required(String);
impl Required {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty (not provided by server)
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let r = Required::from_utf8("60 Required\r\n".as_bytes()).unwrap();
assert_eq!(r.message(), Some("Required"));
assert_eq!(r.message_or_default(), "Required");
assert_eq!(r.as_str(), "60 Required\r\n");
assert_eq!(r.as_bytes(), "60 Required\r\n".as_bytes());
let r = Required::from_utf8("60\r\n".as_bytes()).unwrap();
assert_eq!(r.message(), None);
assert_eq!(r.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(r.as_str(), "60\r\n");
assert_eq!(r.as_bytes(), "60\r\n".as_bytes());
// err
assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err());
assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err());
assert!(Required::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(Required::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,70 @@
use std::{
fmt::{Display, Formatter, Result},
str::Utf8Error,
};
#[derive(Debug)]
pub enum Error {
Certificate(super::certificate::Error),
Code(u8),
Failure(super::failure::Error),
Input(super::input::Error),
Protocol(Vec<u8>),
Redirect(super::redirect::Error),
Stream(glib::Error, Vec<u8>),
Success(super::success::Error),
Utf8Error(Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Certificate(e) => {
write!(f, "Certificate error: {e}")
}
Self::Code(b) => {
write!(f, "Unexpected status code byte: {b}")
}
Self::Failure(e) => {
write!(f, "Failure error: {e}")
}
Self::Input(e) => {
write!(f, "Input error: {e}")
}
Self::Protocol(..) => {
write!(f, "Protocol error")
}
Self::Redirect(e) => {
write!(f, "Redirect error: {e}")
}
Self::Stream(e, ..) => {
write!(f, "I/O stream error: {e}")
}
Self::Success(e) => {
write!(f, "Success error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}
#[derive(Debug)]
pub enum HeaderBytesError {
Len,
End,
}
impl Display for HeaderBytesError {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Len => {
write!(f, "Unexpected header length")
}
Self::End => {
write!(f, "Unexpected header end")
}
}
}
}

View file

@ -0,0 +1,89 @@
pub mod error;
pub mod permanent;
pub mod temporary;
pub use error::Error;
pub use permanent::Permanent;
pub use temporary::Temporary;
pub enum Failure {
/// 4* status code group
/// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure
Temporary(Temporary),
/// 5* status code group
/// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure
Permanent(Permanent),
}
impl Failure {
// Constructors
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
match buffer.first() {
Some(b) => match b {
b'4' => match Temporary::from_utf8(buffer) {
Ok(input) => Ok(Self::Temporary(input)),
Err(e) => Err(Error::Temporary(e)),
},
b'5' => match Permanent::from_utf8(buffer) {
Ok(failure) => Ok(Self::Permanent(failure)),
Err(e) => Err(Error::Permanent(e)),
},
b => Err(Error::Code(*b)),
},
None => Err(Error::Protocol),
}
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty
pub fn message(&self) -> Option<&str> {
match self {
Self::Permanent(permanent) => permanent.message(),
Self::Temporary(temporary) => temporary.message(),
}
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
match self {
Self::Permanent(permanent) => permanent.message_or_default(),
Self::Temporary(temporary) => temporary.message_or_default(),
}
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
match self {
Self::Permanent(permanent) => permanent.as_str(),
Self::Temporary(temporary) => temporary.as_str(),
}
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Permanent(permanent) => permanent.as_bytes(),
Self::Temporary(temporary) => temporary.as_bytes(),
}
}
}
#[test]
fn test() {
fn t(source: String, message: Option<&str>) {
let b = source.as_bytes();
let i = Failure::from_utf8(b).unwrap();
assert_eq!(i.message(), message);
assert_eq!(i.as_str(), source);
assert_eq!(i.as_bytes(), b);
}
for code in [40, 41, 42, 43, 44, 50, 51, 52, 53, 59] {
t(format!("{code} Message\r\n"), Some("Message"));
t(format!("{code}\r\n"), None);
}
}

View file

@ -0,0 +1,28 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code(u8),
Permanent(super::permanent::Error),
Protocol,
Temporary(super::temporary::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code(b) => {
write!(f, "Unexpected status code byte: {b}")
}
Self::Permanent(e) => {
write!(f, "Permanent failure group error: {e}")
}
Self::Protocol => {
write!(f, "Protocol error")
}
Self::Temporary(e) => {
write!(f, "Temporary failure group error: {e}")
}
}
}
}

View file

@ -0,0 +1,126 @@
pub mod bad_request;
pub mod default;
pub mod error;
pub mod gone;
pub mod not_found;
pub mod proxy_request_refused;
pub use bad_request::BadRequest;
pub use default::Default;
pub use error::Error;
pub use gone::Gone;
pub use not_found::NotFound;
pub use proxy_request_refused::ProxyRequestRefused;
const CODE: u8 = b'5';
/// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure
pub enum Permanent {
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-50
Default(Default),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found
NotFound(NotFound),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone
Gone(Gone),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused
ProxyRequestRefused(ProxyRequestRefused),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request
BadRequest(BadRequest),
}
impl Permanent {
// Constructors
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
match buffer.first() {
Some(b) => match *b {
CODE => match buffer.get(1) {
Some(b) => match *b {
b'0' => Ok(Self::Default(
Default::from_utf8(buffer).map_err(Error::Default)?,
)),
b'1' => Ok(Self::NotFound(
NotFound::from_utf8(buffer).map_err(Error::NotFound)?,
)),
b'2' => Ok(Self::Gone(Gone::from_utf8(buffer).map_err(Error::Gone)?)),
b'3' => Ok(Self::ProxyRequestRefused(
ProxyRequestRefused::from_utf8(buffer)
.map_err(Error::ProxyRequestRefused)?,
)),
b'9' => Ok(Self::BadRequest(
BadRequest::from_utf8(buffer).map_err(Error::BadRequest)?,
)),
b => Err(Error::SecondByte(b)),
},
None => Err(Error::UndefinedSecondByte),
},
b => Err(Error::FirstByte(b)),
},
None => Err(Error::UndefinedFirstByte),
}
}
// Getters
pub fn message(&self) -> Option<&str> {
match self {
Self::Default(default) => default.message(),
Self::NotFound(not_found) => not_found.message(),
Self::Gone(gone) => gone.message(),
Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.message(),
Self::BadRequest(bad_request) => bad_request.message(),
}
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
match self {
Self::Default(default) => default.message_or_default(),
Self::NotFound(not_found) => not_found.message_or_default(),
Self::Gone(gone) => gone.message_or_default(),
Self::ProxyRequestRefused(proxy_request_refused) => {
proxy_request_refused.message_or_default()
}
Self::BadRequest(bad_request) => bad_request.message_or_default(),
}
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
match self {
Self::Default(default) => default.as_str(),
Self::NotFound(not_found) => not_found.as_str(),
Self::Gone(gone) => gone.as_str(),
Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.as_str(),
Self::BadRequest(bad_request) => bad_request.as_str(),
}
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Default(default) => default.as_bytes(),
Self::NotFound(not_found) => not_found.as_bytes(),
Self::Gone(gone) => gone.as_bytes(),
Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.as_bytes(),
Self::BadRequest(bad_request) => bad_request.as_bytes(),
}
}
}
#[test]
fn test() {
fn t(source: String, message: Option<&str>) {
let b = source.as_bytes();
let i = Permanent::from_utf8(b).unwrap();
assert_eq!(i.message(), message);
assert_eq!(i.as_str(), source);
assert_eq!(i.as_bytes(), b);
}
for code in [50, 51, 52, 53, 59] {
t(format!("{code} Message\r\n"), Some("Message"));
t(format!("{code}\r\n"), None);
}
}

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code
pub const CODE: &[u8] = b"59";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Bad request";
/// Hold header `String` for [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct BadRequest(String);
impl BadRequest {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let br = BadRequest::from_utf8("59 Message\r\n".as_bytes()).unwrap();
assert_eq!(br.message(), Some("Message"));
assert_eq!(br.message_or_default(), "Message");
assert_eq!(br.as_str(), "59 Message\r\n");
assert_eq!(br.as_bytes(), "59 Message\r\n".as_bytes());
let br = BadRequest::from_utf8("59\r\n".as_bytes()).unwrap();
assert_eq!(br.message(), None);
assert_eq!(br.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(br.as_str(), "59\r\n");
assert_eq!(br.as_bytes(), "59\r\n".as_bytes());
// err
assert!(BadRequest::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(BadRequest::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(BadRequest::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) status code
pub const CODE: &[u8] = b"50";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Permanent error";
/// Hold header `String` for [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct Default(String);
impl Default {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let d = Default::from_utf8("50 Message\r\n".as_bytes()).unwrap();
assert_eq!(d.message(), Some("Message"));
assert_eq!(d.message_or_default(), "Message");
assert_eq!(d.as_str(), "50 Message\r\n");
assert_eq!(d.as_bytes(), "50 Message\r\n".as_bytes());
let d = Default::from_utf8("50\r\n".as_bytes()).unwrap();
assert_eq!(d.message(), None);
assert_eq!(d.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(d.as_str(), "50\r\n");
assert_eq!(d.as_bytes(), "50\r\n".as_bytes());
// err
assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(Default::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,48 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
BadRequest(super::bad_request::Error),
Default(super::default::Error),
FirstByte(u8),
Gone(super::gone::Error),
NotFound(super::not_found::Error),
ProxyRequestRefused(super::proxy_request_refused::Error),
SecondByte(u8),
UndefinedFirstByte,
UndefinedSecondByte,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::BadRequest(e) => {
write!(f, "BadRequest parse error: {e}")
}
Self::Default(e) => {
write!(f, "Default parse error: {e}")
}
Self::FirstByte(b) => {
write!(f, "Unexpected first byte: {b}")
}
Self::Gone(e) => {
write!(f, "Gone parse error: {e}")
}
Self::NotFound(e) => {
write!(f, "NotFound parse error: {e}")
}
Self::ProxyRequestRefused(e) => {
write!(f, "ProxyRequestRefused parse error: {e}")
}
Self::SecondByte(b) => {
write!(f, "Unexpected second byte: {b}")
}
Self::UndefinedFirstByte => {
write!(f, "Undefined first byte")
}
Self::UndefinedSecondByte => {
write!(f, "Undefined second byte")
}
}
}
}

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code
pub const CODE: &[u8] = b"52";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Resource gone";
/// Hold header `String` for [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct Gone(String);
impl Gone {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let g = Gone::from_utf8("52 Message\r\n".as_bytes()).unwrap();
assert_eq!(g.message(), Some("Message"));
assert_eq!(g.message_or_default(), "Message");
assert_eq!(g.as_str(), "52 Message\r\n");
assert_eq!(g.as_bytes(), "52 Message\r\n".as_bytes());
let g = Gone::from_utf8("52\r\n".as_bytes()).unwrap();
assert_eq!(g.message(), None);
assert_eq!(g.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(g.as_str(), "52\r\n");
assert_eq!(g.as_bytes(), "52\r\n".as_bytes());
// err
assert!(Gone::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(Gone::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(Gone::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code
pub const CODE: &[u8] = b"51";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Not Found";
/// Hold header `String` for [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct NotFound(String);
impl NotFound {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let nf = NotFound::from_utf8("51 Message\r\n".as_bytes()).unwrap();
assert_eq!(nf.message(), Some("Message"));
assert_eq!(nf.message_or_default(), "Message");
assert_eq!(nf.as_str(), "51 Message\r\n");
assert_eq!(nf.as_bytes(), "51 Message\r\n".as_bytes());
let nf = NotFound::from_utf8("51\r\n".as_bytes()).unwrap();
assert_eq!(nf.message(), None);
assert_eq!(nf.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(nf.as_str(), "51\r\n");
assert_eq!(nf.as_bytes(), "51\r\n".as_bytes());
// err
assert!(NotFound::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(NotFound::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(NotFound::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code
pub const CODE: &[u8] = b"53";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Proxy request refused";
/// Hold header `String` for [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct ProxyRequestRefused(String);
impl ProxyRequestRefused {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let prf = ProxyRequestRefused::from_utf8("53 Message\r\n".as_bytes()).unwrap();
assert_eq!(prf.message(), Some("Message"));
assert_eq!(prf.message_or_default(), "Message");
assert_eq!(prf.as_str(), "53 Message\r\n");
assert_eq!(prf.as_bytes(), "53 Message\r\n".as_bytes());
let prf = ProxyRequestRefused::from_utf8("53\r\n".as_bytes()).unwrap();
assert_eq!(prf.message(), None);
assert_eq!(prf.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(prf.as_str(), "53\r\n");
assert_eq!(prf.as_bytes(), "53\r\n".as_bytes());
// err
assert!(ProxyRequestRefused::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(ProxyRequestRefused::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(ProxyRequestRefused::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,126 @@
pub mod cgi_error;
pub mod default;
pub mod error;
pub mod proxy_error;
pub mod server_unavailable;
pub mod slow_down;
pub use cgi_error::CgiError;
pub use default::Default;
pub use error::Error;
pub use proxy_error::ProxyError;
pub use server_unavailable::ServerUnavailable;
pub use slow_down::SlowDown;
const CODE: u8 = b'4';
/// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure
pub enum Temporary {
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-40
Default(Default),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable
ServerUnavailable(ServerUnavailable),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error
CgiError(CgiError),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error
ProxyError(ProxyError),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down
SlowDown(SlowDown),
}
impl Temporary {
// Constructors
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
match buffer.first() {
Some(b) => match *b {
CODE => match buffer.get(1) {
Some(b) => match *b {
b'0' => Ok(Self::Default(
Default::from_utf8(buffer).map_err(Error::Default)?,
)),
b'1' => Ok(Self::ServerUnavailable(
ServerUnavailable::from_utf8(buffer)
.map_err(Error::ServerUnavailable)?,
)),
b'2' => Ok(Self::CgiError(
CgiError::from_utf8(buffer).map_err(Error::CgiError)?,
)),
b'3' => Ok(Self::ProxyError(
ProxyError::from_utf8(buffer).map_err(Error::ProxyError)?,
)),
b'4' => Ok(Self::SlowDown(
SlowDown::from_utf8(buffer).map_err(Error::SlowDown)?,
)),
b => Err(Error::SecondByte(b)),
},
None => Err(Error::UndefinedSecondByte),
},
b => Err(Error::FirstByte(b)),
},
None => Err(Error::UndefinedFirstByte),
}
}
// Getters
pub fn message(&self) -> Option<&str> {
match self {
Self::Default(default) => default.message(),
Self::ServerUnavailable(server_unavailable) => server_unavailable.message(),
Self::CgiError(cgi_error) => cgi_error.message(),
Self::ProxyError(proxy_error) => proxy_error.message(),
Self::SlowDown(slow_down) => slow_down.message(),
}
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
match self {
Self::Default(default) => default.message_or_default(),
Self::ServerUnavailable(server_unavailable) => server_unavailable.message_or_default(),
Self::CgiError(cgi_error) => cgi_error.message_or_default(),
Self::ProxyError(proxy_error) => proxy_error.message_or_default(),
Self::SlowDown(slow_down) => slow_down.message_or_default(),
}
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
match self {
Self::Default(default) => default.as_str(),
Self::ServerUnavailable(server_unavailable) => server_unavailable.as_str(),
Self::CgiError(cgi_error) => cgi_error.as_str(),
Self::ProxyError(proxy_error) => proxy_error.as_str(),
Self::SlowDown(slow_down) => slow_down.as_str(),
}
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Default(default) => default.as_bytes(),
Self::ServerUnavailable(server_unavailable) => server_unavailable.as_bytes(),
Self::CgiError(cgi_error) => cgi_error.as_bytes(),
Self::ProxyError(proxy_error) => proxy_error.as_bytes(),
Self::SlowDown(slow_down) => slow_down.as_bytes(),
}
}
}
#[test]
fn test() {
fn t(source: String, message: Option<&str>) {
let b = source.as_bytes();
let i = Temporary::from_utf8(b).unwrap();
assert_eq!(i.message(), message);
assert_eq!(i.as_str(), source);
assert_eq!(i.as_bytes(), b);
}
for code in [40, 41, 42, 43, 44] {
t(format!("{code} Message\r\n"), Some("Message"));
t(format!("{code}\r\n"), None);
}
}

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code
pub const CODE: &[u8] = b"42";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "CGI Error";
/// Hold header `String` for [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct CgiError(String);
impl CgiError {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let ce = CgiError::from_utf8("42 Message\r\n".as_bytes()).unwrap();
assert_eq!(ce.message(), Some("Message"));
assert_eq!(ce.message_or_default(), "Message");
assert_eq!(ce.as_str(), "42 Message\r\n");
assert_eq!(ce.as_bytes(), "42 Message\r\n".as_bytes());
let ce = CgiError::from_utf8("42\r\n".as_bytes()).unwrap();
assert_eq!(ce.message(), None);
assert_eq!(ce.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(ce.as_str(), "42\r\n");
assert_eq!(ce.as_bytes(), "42\r\n".as_bytes());
// err
assert!(CgiError::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(CgiError::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(CgiError::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) status code
pub const CODE: &[u8] = b"40";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Temporary error";
/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct Default(String);
impl Default {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let d = Default::from_utf8("40 Message\r\n".as_bytes()).unwrap();
assert_eq!(d.message(), Some("Message"));
assert_eq!(d.message_or_default(), "Message");
assert_eq!(d.as_str(), "40 Message\r\n");
assert_eq!(d.as_bytes(), "40 Message\r\n".as_bytes());
let d = Default::from_utf8("40\r\n".as_bytes()).unwrap();
assert_eq!(d.message(), None);
assert_eq!(d.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(d.as_str(), "40\r\n");
assert_eq!(d.as_bytes(), "40\r\n".as_bytes());
// err
assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(Default::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,48 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
CgiError(super::cgi_error::Error),
Default(super::default::Error),
FirstByte(u8),
ProxyError(super::proxy_error::Error),
SecondByte(u8),
ServerUnavailable(super::server_unavailable::Error),
SlowDown(super::slow_down::Error),
UndefinedFirstByte,
UndefinedSecondByte,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::CgiError(e) => {
write!(f, "CgiError parse error: {e}")
}
Self::Default(e) => {
write!(f, "Default parse error: {e}")
}
Self::FirstByte(b) => {
write!(f, "Unexpected first byte: {b}")
}
Self::ProxyError(e) => {
write!(f, "ProxyError parse error: {e}")
}
Self::SecondByte(b) => {
write!(f, "Unexpected second byte: {b}")
}
Self::ServerUnavailable(e) => {
write!(f, "ServerUnavailable parse error: {e}")
}
Self::SlowDown(e) => {
write!(f, "SlowDown parse error: {e}")
}
Self::UndefinedFirstByte => {
write!(f, "Undefined first byte")
}
Self::UndefinedSecondByte => {
write!(f, "Undefined second byte")
}
}
}
}

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code
pub const CODE: &[u8] = b"43";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Proxy error";
/// Hold header `String` for [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct ProxyError(String);
impl ProxyError {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let pe = ProxyError::from_utf8("43 Message\r\n".as_bytes()).unwrap();
assert_eq!(pe.message(), Some("Message"));
assert_eq!(pe.message_or_default(), "Message");
assert_eq!(pe.as_str(), "43 Message\r\n");
assert_eq!(pe.as_bytes(), "43 Message\r\n".as_bytes());
let pe = ProxyError::from_utf8("43\r\n".as_bytes()).unwrap();
assert_eq!(pe.message(), None);
assert_eq!(pe.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(pe.as_str(), "43\r\n");
assert_eq!(pe.as_bytes(), "43\r\n".as_bytes());
// err
assert!(ProxyError::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(ProxyError::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(ProxyError::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,81 @@
pub mod error;
pub use error::Error;
/// [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable)
/// temporary error status code
pub const CODE: &[u8] = b"41";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Server unavailable";
/// Hold header `String` for [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable)
/// temporary error status code
///
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct ServerUnavailable(String);
impl ServerUnavailable {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let su = ServerUnavailable::from_utf8("41 Message\r\n".as_bytes()).unwrap();
assert_eq!(su.message(), Some("Message"));
assert_eq!(su.message_or_default(), "Message");
assert_eq!(su.as_str(), "41 Message\r\n");
assert_eq!(su.as_bytes(), "41 Message\r\n".as_bytes());
let su = ServerUnavailable::from_utf8("41\r\n".as_bytes()).unwrap();
assert_eq!(su.message(), None);
assert_eq!(su.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(su.as_str(), "41\r\n");
assert_eq!(su.as_bytes(), "41\r\n".as_bytes());
// err
assert!(ServerUnavailable::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(ServerUnavailable::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(ServerUnavailable::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,81 @@
pub mod error;
pub use error::Error;
/// [Slow Down](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down)
/// temporary error status code
pub const CODE: &[u8] = b"44";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Slow down";
/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down)
/// temporary error status code
///
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct SlowDown(String);
impl SlowDown {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let sd = SlowDown::from_utf8("44 Message\r\n".as_bytes()).unwrap();
assert_eq!(sd.message(), Some("Message"));
assert_eq!(sd.message_or_default(), "Message");
assert_eq!(sd.as_str(), "44 Message\r\n");
assert_eq!(sd.as_bytes(), "44 Message\r\n".as_bytes());
let sd = SlowDown::from_utf8("44\r\n".as_bytes()).unwrap();
assert_eq!(sd.message(), None);
assert_eq!(sd.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(sd.as_str(), "44\r\n");
assert_eq!(sd.as_bytes(), "44\r\n".as_bytes());
// err
assert!(SlowDown::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(SlowDown::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(SlowDown::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,92 @@
pub mod default;
pub mod error;
pub mod sensitive;
pub use default::Default;
pub use error::Error;
pub use sensitive::Sensitive;
const CODE: u8 = b'1';
/// [Input expected](https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected)
pub enum Input {
Default(Default),
Sensitive(Sensitive),
}
impl Input {
// Constructors
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
match buffer.first() {
Some(b) => match *b {
CODE => match buffer.get(1) {
Some(b) => match *b {
b'0' => Ok(Self::Default(
Default::from_utf8(buffer).map_err(Error::Default)?,
)),
b'1' => Ok(Self::Sensitive(
Sensitive::from_utf8(buffer).map_err(Error::Sensitive)?,
)),
b => Err(Error::SecondByte(b)),
},
None => Err(Error::UndefinedSecondByte),
},
b => Err(Error::FirstByte(b)),
},
None => Err(Error::UndefinedFirstByte),
}
}
// Getters
pub fn message(&self) -> Option<&str> {
match self {
Self::Default(default) => default.message(),
Self::Sensitive(sensitive) => sensitive.message(),
}
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
match self {
Self::Default(default) => default.message_or_default(),
Self::Sensitive(sensitive) => sensitive.message_or_default(),
}
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
match self {
Self::Default(default) => default.as_str(),
Self::Sensitive(sensitive) => sensitive.as_str(),
}
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Default(default) => default.as_bytes(),
Self::Sensitive(sensitive) => sensitive.as_bytes(),
}
}
}
#[test]
fn test() {
fn t(source: &str, message: Option<&str>) {
let b = source.as_bytes();
let i = Input::from_utf8(b).unwrap();
assert_eq!(i.message(), message);
assert_eq!(i.as_str(), source);
assert_eq!(i.as_bytes(), b);
}
// 10
t("10 Default\r\n", Some("Default"));
t("10\r\n", None);
// 11
t("11 Sensitive\r\n", Some("Sensitive"));
t("11\r\n", None);
}

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code
pub const CODE: &[u8] = b"10";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Input expected";
/// Hold header `String` for [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct Default(String);
impl Default {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let d = Default::from_utf8("10 Default\r\n".as_bytes()).unwrap();
assert_eq!(d.message(), Some("Default"));
assert_eq!(d.message_or_default(), "Default");
assert_eq!(d.as_str(), "10 Default\r\n");
assert_eq!(d.as_bytes(), "10 Default\r\n".as_bytes());
let d = Default::from_utf8("10\r\n".as_bytes()).unwrap();
assert_eq!(d.message(), None);
assert_eq!(d.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(d.as_str(), "10\r\n");
assert_eq!(d.as_bytes(), "10\r\n".as_bytes());
// err
assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(Default::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,36 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Default(super::default::Error),
FirstByte(u8),
SecondByte(u8),
Sensitive(super::sensitive::Error),
UndefinedFirstByte,
UndefinedSecondByte,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Default(e) => {
write!(f, "Default parse error: {e}")
}
Self::FirstByte(b) => {
write!(f, "Unexpected first byte: {b}")
}
Self::SecondByte(b) => {
write!(f, "Unexpected second byte: {b}")
}
Self::Sensitive(e) => {
write!(f, "Sensitive parse error: {e}")
}
Self::UndefinedFirstByte => {
write!(f, "Undefined first byte")
}
Self::UndefinedSecondByte => {
write!(f, "Undefined second byte")
}
}
}
}

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code
pub const CODE: &[u8] = b"11";
/// Default message if the optional value was not provided by the server
/// * useful to skip match cases in external applications,
/// by using `super::message_or_default` method.
pub const DEFAULT_MESSAGE: &str = "Sensitive input expected";
/// Hold header `String` for [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct Sensitive(String);
impl Sensitive {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get optional message for `Self`
/// * return `None` if the message is empty
pub fn message(&self) -> Option<&str> {
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
}
/// Get optional message for `Self`
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
pub fn message_or_default(&self) -> &str {
self.message().unwrap_or(DEFAULT_MESSAGE)
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
&self.0
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let s = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap();
assert_eq!(s.message(), Some("Sensitive"));
assert_eq!(s.message_or_default(), "Sensitive");
assert_eq!(s.as_str(), "11 Sensitive\r\n");
assert_eq!(s.as_bytes(), "11 Sensitive\r\n".as_bytes());
let s = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap();
assert_eq!(s.message(), None);
assert_eq!(s.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(s.as_str(), "11\r\n");
assert_eq!(s.as_bytes(), "11\r\n".as_bytes());
// err
assert!(Sensitive::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(Sensitive::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(Sensitive::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,185 @@
pub mod error;
pub mod permanent;
pub mod temporary;
pub use error::{Error, UriError};
pub use permanent::Permanent;
pub use temporary::Temporary;
// Local dependencies
use glib::{Uri, UriFlags};
const CODE: u8 = b'3';
/// [Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection) statuses
pub enum Redirect {
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection
Temporary(Temporary),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection
Permanent(Permanent),
}
impl Redirect {
// Constructors
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
match buffer.first() {
Some(b) => match *b {
CODE => match buffer.get(1) {
Some(b) => match *b {
b'0' => Ok(Self::Temporary(
Temporary::from_utf8(buffer).map_err(Error::Temporary)?,
)),
b'1' => Ok(Self::Permanent(
Permanent::from_utf8(buffer).map_err(Error::Permanent)?,
)),
b => Err(Error::SecondByte(b)),
},
None => Err(Error::UndefinedSecondByte),
},
b => Err(Error::FirstByte(b)),
},
None => Err(Error::UndefinedFirstByte),
}
}
// Getters
pub fn target(&self) -> Result<&str, Error> {
match self {
Self::Temporary(temporary) => temporary.target().map_err(Error::Temporary),
Self::Permanent(permanent) => permanent.target().map_err(Error::Permanent),
}
}
pub fn as_str(&self) -> &str {
match self {
Self::Temporary(temporary) => temporary.as_str(),
Self::Permanent(permanent) => permanent.as_str(),
}
}
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Temporary(temporary) => temporary.as_bytes(),
Self::Permanent(permanent) => permanent.as_bytes(),
}
}
pub fn uri(&self, base: &Uri) -> Result<Uri, Error> {
match self {
Self::Temporary(temporary) => temporary.uri(base).map_err(Error::Temporary),
Self::Permanent(permanent) => permanent.uri(base).map_err(Error::Permanent),
}
}
}
// Tools
/// Resolve [specification-compatible](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection),
/// absolute [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `target` using `base`
/// * fragment implementation uncompleted @TODO
fn uri(target: &str, base: &Uri) -> Result<Uri, UriError> {
match Uri::build(
UriFlags::NONE,
base.scheme().as_str(),
None, // unexpected
base.host().as_deref(),
base.port(),
base.path().as_str(),
// > If a server sends a redirection in response to a request with a query string,
// > the client MUST NOT apply the query string to the new location
None,
// > A server SHOULD NOT include fragments in redirections,
// > but if one is given, and a client already has a fragment it could apply (from the original URI),
// > it is up to the client which fragment to apply.
None, // @TODO
)
.parse_relative(
&{
// URI started with double slash yet not supported by Glib function
// https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
let t = target;
match t.strip_prefix("//") {
Some(p) => {
let postfix = p.trim_start_matches(":");
format!(
"{}://{}",
base.scheme(),
if postfix.is_empty() {
match base.host() {
Some(h) => format!("{h}/"),
None => return Err(UriError::BaseHost),
}
} else {
postfix.to_string()
}
)
}
None => t.to_string(),
}
},
UriFlags::NONE,
) {
Ok(absolute) => Ok(absolute),
Err(e) => Err(UriError::ParseRelative(e)),
}
}
#[test]
fn test() {
/// Test common assertion rules
fn t(base: &Uri, source: &str, target: &str) {
let b = source.as_bytes();
let r = Redirect::from_utf8(b).unwrap();
assert!(r.uri(base).is_ok_and(|u| u.to_string() == target));
assert_eq!(r.as_str(), source);
assert_eq!(r.as_bytes(), b);
}
// common base
let base = Uri::build(
UriFlags::NONE,
"gemini",
None,
Some("geminiprotocol.net"),
-1,
"/path/",
Some("query"),
Some("fragment"),
);
// codes test
t(
&base,
"30 gemini://geminiprotocol.net/path\r\n",
"gemini://geminiprotocol.net/path",
);
t(
&base,
"31 gemini://geminiprotocol.net/path\r\n",
"gemini://geminiprotocol.net/path",
);
// relative test
t(
&base,
"31 path\r\n",
"gemini://geminiprotocol.net/path/path",
);
t(
&base,
"31 //geminiprotocol.net\r\n",
"gemini://geminiprotocol.net",
);
t(
&base,
"31 //geminiprotocol.net/path\r\n",
"gemini://geminiprotocol.net/path",
);
t(&base, "31 /path\r\n", "gemini://geminiprotocol.net/path");
t(&base, "31 //:\r\n", "gemini://geminiprotocol.net/");
t(&base, "31 //\r\n", "gemini://geminiprotocol.net/");
t(&base, "31 /\r\n", "gemini://geminiprotocol.net/");
t(&base, "31 ../\r\n", "gemini://geminiprotocol.net/");
t(&base, "31 ..\r\n", "gemini://geminiprotocol.net/");
}

View file

@ -0,0 +1,56 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
FirstByte(u8),
Permanent(super::permanent::Error),
SecondByte(u8),
Temporary(super::temporary::Error),
UndefinedFirstByte,
UndefinedSecondByte,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::FirstByte(b) => {
write!(f, "Unexpected first byte: {b}")
}
Self::Permanent(e) => {
write!(f, "Permanent parse error: {e}")
}
Self::SecondByte(b) => {
write!(f, "Unexpected second byte: {b}")
}
Self::Temporary(e) => {
write!(f, "Temporary parse error: {e}")
}
Self::UndefinedFirstByte => {
write!(f, "Undefined first byte")
}
Self::UndefinedSecondByte => {
write!(f, "Undefined second byte")
}
}
}
}
/// Handle `super::uri` method
#[derive(Debug)]
pub enum UriError {
BaseHost,
ParseRelative(glib::Error),
}
impl Display for UriError {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::BaseHost => {
write!(f, "URI base host required")
}
Self::ParseRelative(e) => {
write!(f, "URI parse relative error: {e}")
}
}
}
}

View file

@ -0,0 +1,82 @@
pub mod error;
pub use error::Error;
// Local dependencies
use glib::Uri;
/// [Permanent Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection) status code
pub const CODE: &[u8] = b"31";
/// Hold header `String` for [Permanent Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct Permanent(String);
impl Permanent {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get raw target for `Self`
/// * return `Err` if the required target is empty
pub fn target(&self) -> Result<&str, Error> {
self.0
.get(2..)
.map(|s| s.trim())
.filter(|x| !x.is_empty())
.ok_or(Error::TargetEmpty)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
pub fn uri(&self, base: &Uri) -> Result<Uri, Error> {
super::uri(self.target()?, base).map_err(Error::Uri)
}
}
#[test]
fn test() {
const BUFFER: &str = "31 gemini://geminiprotocol.net/path\r\n";
let bytes = BUFFER.as_bytes();
let base = Uri::build(
glib::UriFlags::NONE,
"gemini",
None,
Some("geminiprotocol.net"),
-1,
"/path/",
Some("query"),
Some("fragment"),
);
let permanent = Permanent::from_utf8(bytes).unwrap();
assert_eq!(permanent.as_str(), BUFFER);
assert_eq!(permanent.as_bytes(), bytes);
assert!(permanent.target().is_ok());
assert!(
permanent
.uri(&base)
.is_ok_and(|u| u.to_string() == "gemini://geminiprotocol.net/path")
);
assert!(Permanent::from_utf8("32 gemini://geminiprotocol.net/path\r\n".as_bytes()).is_err());
}

View file

@ -0,0 +1,32 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
TargetEmpty,
Uri(super::super::UriError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::TargetEmpty => {
write!(f, "Expected target is empty")
}
Self::Uri(e) => {
write!(f, "URI parse error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,82 @@
pub mod error;
pub use error::Error;
// Local dependencies
use glib::Uri;
/// [Temporary Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection) status code
pub const CODE: &[u8] = b"30";
/// Hold header `String` for [Temporary Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct Temporary(String);
impl Temporary {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Get raw target for `Self`
/// * return `Err` if the required target is empty
pub fn target(&self) -> Result<&str, Error> {
self.0
.get(2..)
.map(|s| s.trim())
.filter(|x| !x.is_empty())
.ok_or(Error::TargetEmpty)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
pub fn uri(&self, base: &Uri) -> Result<Uri, Error> {
super::uri(self.target()?, base).map_err(Error::Uri)
}
}
#[test]
fn test() {
const BUFFER: &str = "30 gemini://geminiprotocol.net/path\r\n";
let bytes = BUFFER.as_bytes();
let base = Uri::build(
glib::UriFlags::NONE,
"gemini",
None,
Some("geminiprotocol.net"),
-1,
"/path/",
Some("query"),
Some("fragment"),
);
let temporary = Temporary::from_utf8(BUFFER.as_bytes()).unwrap();
assert_eq!(temporary.as_str(), BUFFER);
assert_eq!(temporary.as_bytes(), bytes);
assert!(temporary.target().is_ok());
assert!(
temporary
.uri(&base)
.is_ok_and(|u| u.to_string() == "gemini://geminiprotocol.net/path")
);
assert!(Temporary::from_utf8("32 gemini://geminiprotocol.net/path\r\n".as_bytes()).is_err())
}

View file

@ -0,0 +1,32 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(crate::client::connection::response::HeaderBytesError),
TargetEmpty,
Uri(super::super::UriError),
Utf8Error(std::str::Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::TargetEmpty => {
write!(f, "Expected target is empty")
}
Self::Uri(e) => {
write!(f, "URI parse error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 decode error: {e}")
}
}
}
}

View file

@ -0,0 +1,76 @@
pub mod default;
pub mod error;
pub use default::Default;
pub use error::Error;
const CODE: u8 = b'2';
pub enum Success {
Default(Default),
// reserved for 2* codes
}
impl Success {
// Constructors
/// Parse new `Self` from buffer bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if buffer.first().is_none_or(|b| *b != CODE) {
return Err(Error::Code);
}
match Default::from_utf8(buffer) {
Ok(default) => Ok(Self::Default(default)),
Err(e) => Err(Error::Default(e)),
}
}
// Getters
/// Get header bytes for `Self` type
pub fn as_header_bytes(&self) -> &[u8] {
match self {
Self::Default(default) => default.header.as_bytes(),
}
}
/// Get header string for `Self` type
pub fn as_header_str(&self) -> &str {
match self {
Self::Default(default) => default.header.as_str(),
}
}
/// Get parsed MIME for `Self` type
///
/// * high-level method, useful to skip extra match case constructions;
/// * at this moment, Gemini protocol has only one status code in this scope,\
/// this method would be deprecated in future, use on your own risk!
pub fn mime(&self) -> Result<String, Error> {
match self {
Self::Default(default) => default
.header
.mime()
.map_err(|e| Error::Default(default::Error::Header(e))),
}
}
}
#[test]
fn test() {
let r = "20 text/gemini; charset=utf-8; lang=en\r\n";
let b = r.as_bytes();
let s = Success::from_utf8(b).unwrap();
match s {
Success::Default(ref d) => {
assert_eq!(d.header.mime().unwrap(), "text/gemini");
assert!(d.content.is_empty())
}
}
assert_eq!(s.as_header_bytes(), b);
assert_eq!(s.as_header_str(), r);
assert_eq!(s.mime().unwrap(), "text/gemini");
assert!(Success::from_utf8("40 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err())
}

View file

@ -0,0 +1,51 @@
pub mod error;
pub mod header;
pub use error::Error;
pub use header::Header;
/// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) status code
pub const CODE: &[u8] = b"20";
/// Holder for [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) status code
/// * this response type MAY contain body data
/// * the header has closed members to require valid construction
pub struct Default {
/// Formatted header holder with additional API
pub header: Header,
/// Default success response MAY include body data
/// * if the `Request` constructed with `Mode::HeaderOnly` flag,\
/// this value wants to be processed manually, using external application logic (specific for content-type)
pub content: Vec<u8>,
}
impl Default {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
let header = Header::from_utf8(buffer).map_err(Error::Header)?;
Ok(Self {
content: buffer
.get(header.as_bytes().len()..)
.filter(|s| !s.is_empty())
.map_or(Vec::new(), |v| v.to_vec()),
header,
})
}
}
#[test]
fn test() {
let d = Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap();
assert_eq!(d.header.mime().unwrap(), "text/gemini");
assert!(d.content.is_empty());
let d =
Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\ndata".as_bytes()).unwrap();
assert_eq!(d.header.mime().unwrap(), "text/gemini");
assert_eq!(d.content.len(), 4);
}

View file

@ -0,0 +1,20 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(super::header::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
}
}
}

View file

@ -0,0 +1,60 @@
pub mod error;
pub use error::Error;
pub struct Header(String);
impl Header {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(super::CODE) {
return Err(Error::Code);
}
Ok(Self(
std::str::from_utf8(
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
)
.map_err(Error::Utf8Error)?
.to_string(),
))
}
// Getters
/// Parse content type for `Self`
pub fn mime(&self) -> Result<String, Error> {
glib::Regex::split_simple(
r"^\d{2}\s([^\/]+\/[^\s;]+)",
&self.0,
glib::RegexCompileFlags::DEFAULT,
glib::RegexMatchFlags::DEFAULT,
)
.get(1)
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map_or(Err(Error::Mime), |s| Ok(s.to_lowercase()))
}
/// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
/// Get header string of `Self`
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
#[test]
fn test() {
let s = "20 text/gemini; charset=utf-8; lang=en\r\n";
let b = s.as_bytes();
let h = Header::from_utf8(b).unwrap();
assert_eq!(h.mime().unwrap(), "text/gemini");
assert_eq!(h.as_bytes(), b);
assert_eq!(h.as_str(), s);
assert!(Header::from_utf8("21 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err());
}

View file

@ -0,0 +1,31 @@
use std::{
fmt::{Display, Formatter, Result},
str::Utf8Error,
};
#[derive(Debug)]
pub enum Error {
Code,
Mime,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Mime => {
write!(f, "Unexpected content type")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 error: {e}")
}
}
}
}

View file

@ -0,0 +1,20 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Default(super::default::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Default(e) => {
write!(f, "Header error: {e}")
}
}
}
}

View file

@ -1,8 +1,31 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Close,
Connect,
Input,
Output,
Response,
BufferOverflow,
Connect(gio::NetworkAddress, glib::Error),
Connection(gio::SocketConnection, crate::client::connection::Error),
NetworkAddress(crate::client::connection::request::Error),
Request(
crate::client::connection::Connection,
crate::client::connection::Error,
),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Connect(_, e) => {
write!(f, "Connect error: {e}")
}
Self::Connection(_, e) => {
write!(f, "Connection init error: {e}")
}
Self::NetworkAddress(e) => {
write!(f, "Network address error: {e}")
}
Self::Request(_, e) => {
write!(f, "Connection error: {e}")
}
}
}
}

View file

@ -1,42 +0,0 @@
pub mod body;
pub mod error;
pub mod header;
pub use body::Body;
pub use error::Error;
pub use header::Header;
pub struct Response {
header: Header,
body: Body,
}
impl Response {
/// Create new `client::Response`
pub fn new(header: Header, body: Body) -> Self {
Self { header, body }
}
/// Create new `client::Response` from UTF-8 buffer
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
let header = match Header::from_response(buffer) {
Ok(result) => result,
Err(_) => return Err(Error::Header),
};
let body = match Body::from_response(buffer) {
Ok(result) => result,
Err(_) => return Err(Error::Body),
};
Ok(Self::new(header, body))
}
pub fn header(&self) -> &Header {
&self.header
}
pub fn body(&self) -> &Body {
&self.body
}
}

View file

@ -1,46 +0,0 @@
pub mod error;
pub use error::Error;
use glib::GString;
pub struct Body {
buffer: Vec<u8>,
}
impl Body {
/// Construct from response buffer
pub fn from_response(response: &[u8] /* @TODO */) -> Result<Self, Error> {
let start = Self::start(response)?;
let buffer = match response.get(start..) {
Some(result) => result,
None => return Err(Error::Buffer),
};
Ok(Self {
buffer: Vec::from(buffer),
})
}
// Getters
pub fn buffer(&self) -> &Vec<u8> {
&self.buffer
}
pub fn to_gstring(&self) -> Result<GString, Error> {
match GString::from_utf8(self.buffer.to_vec()) {
Ok(result) => Ok(result),
Err(_) => Err(Error::Decode),
}
}
// Tools
fn start(buffer: &[u8]) -> Result<usize, Error> {
for (offset, &byte) in buffer.iter().enumerate() {
if byte == b'\n' {
return Ok(offset + 1);
}
}
Err(Error::Format)
}
}

View file

@ -1,6 +0,0 @@
pub enum Error {
Buffer,
Decode,
Format,
Status,
}

View file

@ -1,4 +0,0 @@
pub enum Error {
Header,
Body,
}

View file

@ -1,70 +0,0 @@
pub mod error;
pub mod meta;
pub mod mime;
pub mod status;
pub use error::Error;
pub use meta::Meta;
pub use mime::Mime;
pub use status::Status;
pub struct Header {
status: Status,
meta: Option<Meta>,
mime: Option<Mime>,
// @TODO
// charset: Option<Charset>,
// language: Option<Language>,
}
impl Header {
/// Construct from response buffer
/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters
pub fn from_response(response: &[u8] /* @TODO */) -> Result<Self, Error> {
let end = Self::end(response)?;
let buffer = match response.get(..end) {
Some(result) => result,
None => return Err(Error::Buffer),
};
let meta = match Meta::from_header(buffer) {
Ok(result) => Some(result),
Err(_) => None,
};
let mime = mime::from_header(buffer); // optional
// let charset = charset::from_header(buffer); @TODO
// let language = language::from_header(buffer); @TODO
let status = match status::from_header(buffer) {
Ok(result) => result,
Err(_) => return Err(Error::Status),
};
Ok(Self { status, meta, mime })
}
// Getters
pub fn status(&self) -> &Status {
&self.status
}
pub fn mime(&self) -> &Option<Mime> {
&self.mime
}
pub fn meta(&self) -> &Option<Meta> {
&self.meta
}
// Tools
fn end(buffer: &[u8]) -> Result<usize, Error> {
for (offset, &byte) in buffer.iter().enumerate() {
if byte == b'\r' {
return Ok(offset);
}
}
Err(Error::Format)
}
}

View file

@ -1 +0,0 @@
// @TODO

View file

@ -1,5 +0,0 @@
pub enum Error {
Buffer,
Format,
Status,
}

View file

@ -1 +0,0 @@
// @TODO

View file

@ -1,26 +0,0 @@
pub mod error;
pub use error::Error;
use glib::GString;
pub struct Meta {
buffer: Vec<u8>,
}
impl Meta {
pub fn from_header(buffer: &[u8] /* @TODO */) -> Result<Self, Error> {
let buffer = match buffer.get(2..) {
Some(bytes) => bytes.to_vec(),
None => return Err(Error::Undefined),
};
Ok(Self { buffer })
}
pub fn to_gstring(&self) -> Result<GString, Error> {
match GString::from_utf8(self.buffer.clone()) {
Ok(result) => Ok(result),
Err(_) => Err(Error::Undefined),
}
}
}

View file

@ -1,4 +0,0 @@
pub enum Error {
Decode,
Undefined,
}

View file

@ -1,62 +0,0 @@
use glib::{GString, Uri};
use std::path::Path;
pub enum Mime {
TextGemini,
TextPlain,
ImagePng,
ImageGif,
ImageJpeg,
ImageWebp,
} // @TODO
pub fn from_header(buffer: &[u8] /* @TODO */) -> Option<Mime> {
from_string(&match GString::from_utf8(buffer.to_vec()) {
Ok(result) => result,
Err(_) => return None, // @TODO error handler?
})
}
pub fn from_path(path: &Path) -> Option<Mime> {
match path.extension().and_then(|extension| extension.to_str()) {
Some("gmi") | Some("gemini") => Some(Mime::TextGemini),
Some("txt") => Some(Mime::TextPlain),
Some("png") => Some(Mime::ImagePng),
Some("gif") => Some(Mime::ImageGif),
Some("jpeg") | Some("jpg") => Some(Mime::ImageJpeg),
Some("webp") => Some(Mime::ImageWebp),
_ => None,
}
}
pub fn from_string(value: &str) -> Option<Mime> {
if value.contains("text/gemini") {
return Some(Mime::TextGemini);
}
if value.contains("text/plain") {
return Some(Mime::TextPlain);
}
if value.contains("image/gif") {
return Some(Mime::ImageGif);
}
if value.contains("image/jpeg") {
return Some(Mime::ImageJpeg);
}
if value.contains("image/webp") {
return Some(Mime::ImageWebp);
}
if value.contains("image/png") {
return Some(Mime::ImagePng);
}
None
}
pub fn from_uri(uri: &Uri) -> Option<Mime> {
from_path(Path::new(&uri.to_string()))
}

View file

@ -1,31 +0,0 @@
pub mod error;
pub use error::Error;
use glib::GString;
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes
pub enum Status {
Input,
SensitiveInput,
Success,
Redirect,
} // @TODO
pub fn from_header(buffer: &[u8] /* @TODO */) -> Result<Status, Error> {
match buffer.get(0..2) {
Some(bytes) => match GString::from_utf8(bytes.to_vec()) {
Ok(string) => from_string(string.as_str()),
Err(_) => Err(Error::Decode),
},
None => Err(Error::Undefined),
}
}
pub fn from_string(code: &str) -> Result<Status, Error> {
match code {
"10" => Ok(Status::Input),
"11" => Ok(Status::SensitiveInput),
"20" => Ok(Status::Success),
_ => Err(Error::Undefined),
}
}

View file

@ -1,4 +0,0 @@
pub enum Error {
Undefined,
Decode,
}

View file

@ -1,23 +0,0 @@
use gio::{prelude::SocketClientExt, SocketClient, SocketProtocol, TlsCertificateFlags};
pub struct Socket {
gobject: SocketClient,
}
impl Socket {
/// Create new `gio::SocketClient` preset for Gemini Protocol
pub fn new() -> Self {
let gobject = SocketClient::new();
gobject.set_protocol(SocketProtocol::Tcp);
gobject.set_tls_validation_flags(TlsCertificateFlags::INSECURE);
gobject.set_tls(true);
Self { gobject }
}
/// Return ref to `gio::SocketClient` GObject
pub fn gobject(&self) -> &SocketClient {
self.gobject.as_ref()
}
}

3
src/gio.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod file_output_stream;
pub mod memory_input_stream;
pub mod network_address;

View file

@ -0,0 +1,69 @@
pub mod error;
pub mod size;
pub use error::Error;
pub use size::Size;
use gio::{
Cancellable, FileOutputStream, IOStream,
prelude::{IOStreamExt, InputStreamExt, OutputStreamExtManual},
};
use glib::{Bytes, Priority, object::IsA};
/// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
/// to [FileOutputStream](https://docs.gtk.org/gio/class.FileOutputStream.html)
/// * require `IOStream` reference to keep `Connection` active in async thread
pub fn from_stream_async(
io_stream: impl IsA<IOStream>,
file_output_stream: FileOutputStream,
cancellable: Cancellable,
priority: Priority,
mut size: Size,
(on_chunk, on_complete): (
impl Fn(Bytes, usize) + 'static, // on_chunk
impl FnOnce(Result<(FileOutputStream, usize), Error>) + 'static, // on_complete
),
) {
io_stream.input_stream().read_bytes_async(
size.chunk,
priority,
Some(&cancellable.clone()),
move |result| match result {
Ok(bytes) => {
size.total += bytes.len();
on_chunk(bytes.clone(), size.total);
if let Some(limit) = size.limit
&& size.total > limit
{
return on_complete(Err(Error::BytesTotal(size.total, limit)));
}
if bytes.is_empty() {
return on_complete(Ok((file_output_stream, size.total)));
}
// Make sure **all bytes** sent to the destination
// > A partial write is performed with the size of a message block, which is 16kB
// > https://docs.openssl.org/3.0/man3/SSL_write/#notes
file_output_stream.clone().write_all_async(
bytes,
priority,
Some(&cancellable.clone()),
move |result| match result {
Ok(_) => from_stream_async(
io_stream,
file_output_stream,
cancellable,
priority,
size,
(on_chunk, on_complete),
),
Err((b, e)) => on_complete(Err(Error::OutputStream(b, e))),
},
)
}
Err(e) => on_complete(Err(Error::InputStream(e))),
},
)
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
BytesTotal(usize, usize),
InputStream(glib::Error),
OutputStream(glib::Bytes, glib::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::BytesTotal(total, limit) => {
write!(f, "Bytes total limit reached: {total} / {limit}")
}
Self::InputStream(e) => {
write!(f, "Input stream error: {e}")
}
Self::OutputStream(_, e) => {
write!(f, "Output stream error: {e}")
}
}
}
}

View file

@ -0,0 +1,17 @@
/// Mutable bytes count
pub struct Size {
pub chunk: usize,
/// `None` for unlimited
pub limit: Option<usize>,
pub total: usize,
}
impl Default for Size {
fn default() -> Self {
Self {
chunk: 0x10000, // 64KB
limit: None,
total: 0,
}
}
}

View file

@ -0,0 +1,95 @@
pub mod error;
pub mod size;
pub use error::Error;
pub use size::Size;
use gio::{
Cancellable, IOStream, MemoryInputStream,
prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt},
};
use glib::{Priority, object::IsA};
/// Asynchronously create new [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html)
/// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
///
/// **Useful for**
/// * safe read (of memory overflow) to dynamically allocated buffer, where final size of target data unknown
/// * calculate bytes processed on chunk load
pub fn from_stream_async(
io_stream: impl IsA<IOStream>,
priority: Priority,
cancelable: Cancellable,
size: Size,
(on_chunk, on_complete): (
impl Fn(usize, usize) + 'static,
impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static,
),
) {
for_memory_input_stream_async(
MemoryInputStream::new(),
io_stream,
priority,
cancelable,
size,
(on_chunk, on_complete),
);
}
/// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
/// to [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html)
/// * require `IOStream` reference to keep `Connection` active in async thread
pub fn for_memory_input_stream_async(
memory_input_stream: MemoryInputStream,
io_stream: impl IsA<IOStream>,
priority: Priority,
cancellable: Cancellable,
mut size: Size,
(on_chunk, on_complete): (
impl Fn(usize, usize) + 'static,
impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static,
),
) {
io_stream.input_stream().read_bytes_async(
size.chunk,
priority,
Some(&cancellable.clone()),
move |result| match result {
Ok(bytes) => {
let len = bytes.len(); // calculate once
// is end of stream
if len == 0 {
return on_complete(Ok((memory_input_stream, size.total)));
}
// callback chunk function
size.total += len;
on_chunk(len, size.total);
// push bytes into the memory pool
memory_input_stream.add_bytes(&bytes);
// prevent memory overflow
if size.total > size.limit {
return on_complete(Err(Error::BytesTotal(
memory_input_stream,
size.total,
size.limit,
)));
}
// handle next chunk..
for_memory_input_stream_async(
memory_input_stream,
io_stream,
priority,
cancellable,
size,
(on_chunk, on_complete),
)
}
Err(e) => on_complete(Err(Error::InputStream(memory_input_stream, e))),
},
)
}

View file

@ -0,0 +1,20 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
BytesTotal(gio::MemoryInputStream, usize, usize),
InputStream(gio::MemoryInputStream, glib::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::BytesTotal(_, total, limit) => {
write!(f, "Bytes total limit reached: {total} / {limit}")
}
Self::InputStream(_, e) => {
write!(f, "Input stream error: {e}")
}
}
}
}

View file

@ -0,0 +1,16 @@
/// Mutable bytes count
pub struct Size {
pub chunk: usize,
pub limit: usize,
pub total: usize,
}
impl Default for Size {
fn default() -> Self {
Self {
chunk: 0x10000, // 64KB
limit: 0xfffff, // 1 MB
total: 0,
}
}
}

View file

@ -0,0 +1,24 @@
pub mod error;
pub use error::Error;
use gio::NetworkAddress;
use glib::Uri;
/// Create new valid [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) from [Uri](https://docs.gtk.org/glib/struct.Uri.html)
///
/// Useful as:
/// * shared [SocketConnectable](https://docs.gtk.org/gio/iface.SocketConnectable.html) interface
/// * [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) record for TLS connections
pub fn from_uri(uri: &Uri, default_port: u16) -> Result<NetworkAddress, Error> {
Ok(NetworkAddress::new(
&match uri.host() {
Some(host) => host,
None => return Err(Error::Host(uri.to_string())),
},
if uri.port().is_positive() {
uri.port() as u16
} else {
default_port
},
))
}

View file

@ -0,0 +1,16 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Host(String),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Host(url) => {
write!(f, "Host required for {url}")
}
}
}
}

View file

@ -1 +1,18 @@
pub mod client;
pub mod gio;
// Main API
pub use client::Client;
// Global defaults
pub const DEFAULT_PORT: u16 = 1965;
// Debug
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const VERSION_MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");
pub const VERSION_MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
pub const VERSION_PATCH: &str = env!("CARGO_PKG_VERSION_PATCH");