1#![allow(dead_code)]
9
10#[cfg(feature = "alloc")]
11use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
12
13pub const HTTP_PORT: u16 = 80;
19
20pub const HTTPS_PORT: u16 = 443;
22
23pub const DEFAULT_MAX_REDIRECTS: u8 = 10;
25
26pub const DEFAULT_TIMEOUT_MS: u64 = 30_000;
28
29const HTTP_VERSION: &str = "HTTP/1.1";
31
32const DEFAULT_USER_AGENT: &str = "VeridianOS/0.10.6";
34
35pub const MIME_TEXT_HTML: &str = "text/html";
38pub const MIME_APPLICATION_JSON: &str = "application/json";
40pub const MIME_TEXT_PLAIN: &str = "text/plain";
42pub const MIME_APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
44pub const MIME_FORM_URLENCODED: &str = "application/x-www-form-urlencoded";
46
47#[cfg(feature = "alloc")]
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum HttpMethod {
55 Get,
56 Post,
57 Put,
58 Delete,
59 Head,
60 Patch,
61 Options,
62}
63
64#[cfg(feature = "alloc")]
65impl HttpMethod {
66 pub fn as_str(&self) -> &'static str {
68 match self {
69 HttpMethod::Get => "GET",
70 HttpMethod::Post => "POST",
71 HttpMethod::Put => "PUT",
72 HttpMethod::Delete => "DELETE",
73 HttpMethod::Head => "HEAD",
74 HttpMethod::Patch => "PATCH",
75 HttpMethod::Options => "OPTIONS",
76 }
77 }
78}
79
80#[cfg(feature = "alloc")]
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct ParsedUrl {
88 pub scheme: String,
90 pub host: String,
92 pub port: u16,
94 pub path: String,
96 pub query: Option<String>,
98 pub is_https: bool,
100}
101
102#[cfg(feature = "alloc")]
103impl ParsedUrl {
104 pub fn parse(url: &str) -> Result<Self, HttpError> {
109 let (scheme, rest) = if let Some(r) = url.strip_prefix("https://") {
110 ("https", r)
111 } else if let Some(r) = url.strip_prefix("http://") {
112 ("http", r)
113 } else {
114 return Err(HttpError::InvalidUrl);
115 };
116
117 let is_https = scheme == "https";
118 let default_port: u16 = if is_https { HTTPS_PORT } else { HTTP_PORT };
119
120 let (authority, path_and_query) = match rest.find('/') {
122 Some(idx) => (&rest[..idx], &rest[idx..]),
123 None => (rest, "/"),
124 };
125
126 let (path, query) = match path_and_query.find('?') {
128 Some(idx) => (&path_and_query[..idx], Some(&path_and_query[idx + 1..])),
129 None => (path_and_query, None),
130 };
131
132 let (host, port) = if let Some(colon_idx) = authority.rfind(':') {
134 let port_str = &authority[colon_idx + 1..];
135 if let Ok(p) = parse_u16(port_str) {
136 (&authority[..colon_idx], p)
137 } else {
138 (authority, default_port)
139 }
140 } else {
141 (authority, default_port)
142 };
143
144 if host.is_empty() {
145 return Err(HttpError::InvalidUrl);
146 }
147
148 let path_str = if path.is_empty() {
149 String::from("/")
150 } else {
151 String::from(path)
152 };
153
154 Ok(ParsedUrl {
155 scheme: String::from(scheme),
156 host: String::from(host),
157 port,
158 path: path_str,
159 query: query.map(String::from),
160 is_https,
161 })
162 }
163
164 pub fn request_path(&self) -> String {
166 match &self.query {
167 Some(q) => format!("{}?{}", self.path, q),
168 None => self.path.clone(),
169 }
170 }
171}
172
173#[cfg(feature = "alloc")]
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub enum HttpError {
181 InvalidUrl,
183 InvalidStatusLine,
185 InvalidHeader,
187 InvalidChunkSize,
189 BodyTooLarge,
191 TooManyRedirects,
193 Timeout,
195 ParseError(String),
197}
198
199#[cfg(feature = "alloc")]
205#[derive(Debug, Clone)]
206pub struct HttpRequest {
207 pub method: HttpMethod,
209 pub url: ParsedUrl,
211 pub headers: BTreeMap<String, String>,
213 pub body: Option<Vec<u8>>,
215}
216
217#[cfg(feature = "alloc")]
218impl HttpRequest {
219 pub fn new(method: HttpMethod, url: &str) -> Result<Self, HttpError> {
221 let parsed = ParsedUrl::parse(url)?;
222 let mut headers = BTreeMap::new();
223
224 if parsed.port != HTTP_PORT && parsed.port != HTTPS_PORT {
226 headers.insert(
227 String::from("host"),
228 format!("{}:{}", parsed.host, parsed.port),
229 );
230 } else {
231 headers.insert(String::from("host"), parsed.host.clone());
232 }
233
234 headers.insert(String::from("user-agent"), String::from(DEFAULT_USER_AGENT));
235 headers.insert(String::from("accept"), String::from("*/*"));
236 headers.insert(String::from("connection"), String::from("keep-alive"));
237
238 Ok(HttpRequest {
239 method,
240 url: parsed,
241 headers,
242 body: None,
243 })
244 }
245
246 pub fn set_header(&mut self, name: &str, value: &str) {
248 self.headers.insert(to_lowercase(name), String::from(value));
249 }
250
251 pub fn set_body(&mut self, body: Vec<u8>) {
253 let len = body.len();
254 self.body = Some(body);
255 self.headers
256 .insert(String::from("content-length"), uint_to_string(len));
257 }
258
259 pub fn set_body_str(&mut self, body: &str) {
261 self.set_body(Vec::from(body.as_bytes()));
262 }
263
264 pub fn set_content_type(&mut self, mime: &str) {
266 self.set_header("content-type", mime);
267 }
268
269 pub fn set_basic_auth(&mut self, user: &str, password: &str) {
271 let credentials = format!("{}:{}", user, password);
272 let encoded = base64_encode(credentials.as_bytes());
273 self.set_header("authorization", &format!("Basic {}", encoded));
274 }
275
276 pub fn set_cookies(&mut self, cookies: &[(&str, &str)]) {
278 if cookies.is_empty() {
279 return;
280 }
281 let mut cookie_str = String::new();
282 for (i, (name, value)) in cookies.iter().enumerate() {
283 if i > 0 {
284 cookie_str.push_str("; ");
285 }
286 cookie_str.push_str(name);
287 cookie_str.push('=');
288 cookie_str.push_str(value);
289 }
290 self.set_header("cookie", &cookie_str);
291 }
292
293 pub fn serialize(&self) -> Vec<u8> {
295 let mut buf = Vec::with_capacity(256);
296
297 let request_line = format!(
299 "{} {} {}\r\n",
300 self.method.as_str(),
301 self.url.request_path(),
302 HTTP_VERSION,
303 );
304 buf.extend_from_slice(request_line.as_bytes());
305
306 for (name, value) in &self.headers {
308 buf.extend_from_slice(name.as_bytes());
309 buf.extend_from_slice(b": ");
310 buf.extend_from_slice(value.as_bytes());
311 buf.extend_from_slice(b"\r\n");
312 }
313
314 buf.extend_from_slice(b"\r\n");
316
317 if let Some(ref body) = self.body {
319 buf.extend_from_slice(body);
320 }
321
322 buf
323 }
324}
325
326#[cfg(feature = "alloc")]
332#[derive(Debug, Clone)]
333pub struct HttpResponse {
334 pub version: String,
336 pub status_code: u16,
338 pub reason: String,
340 pub headers: BTreeMap<String, String>,
342 pub body: Vec<u8>,
344}
345
346#[cfg(feature = "alloc")]
347impl HttpResponse {
348 pub fn is_redirect(&self) -> bool {
350 matches!(self.status_code, 301 | 302 | 307 | 308)
351 }
352
353 pub fn redirect_location(&self) -> Option<&String> {
355 self.headers.get("location")
356 }
357
358 pub fn is_keep_alive(&self) -> bool {
360 if let Some(conn) = self.headers.get("connection") {
361 let lower = to_lowercase(conn);
362 lower.contains("keep-alive")
363 } else {
364 self.version.contains("1.1")
366 }
367 }
368
369 pub fn content_type(&self) -> Option<&String> {
371 self.headers.get("content-type")
372 }
373
374 pub fn content_length(&self) -> Option<usize> {
376 self.headers
377 .get("content-length")
378 .and_then(|v| parse_usize(v).ok())
379 }
380
381 pub fn content_encoding(&self) -> ContentEncoding {
383 match self.headers.get("content-encoding") {
384 Some(v) if to_lowercase(v).contains("gzip") => ContentEncoding::Gzip,
385 Some(v) if to_lowercase(v).contains("deflate") => ContentEncoding::Deflate,
386 _ => ContentEncoding::Identity,
387 }
388 }
389
390 pub fn body_as_str(&self) -> Option<&str> {
392 core::str::from_utf8(&self.body).ok()
393 }
394}
395
396#[cfg(feature = "alloc")]
402#[derive(Debug, Clone, Copy, PartialEq, Eq)]
403pub enum ContentEncoding {
404 Identity,
406 Gzip,
408 Deflate,
410}
411
412#[cfg(feature = "alloc")]
418#[derive(Debug, Clone, PartialEq, Eq)]
419pub enum ParseState {
420 StatusLine,
422 Headers,
424 Body,
426 ChunkedBody,
428 Complete,
430}
431
432#[cfg(feature = "alloc")]
437pub struct ResponseParser {
438 state: ParseState,
439 buffer: Vec<u8>,
440 version: String,
441 status_code: u16,
442 reason: String,
443 headers: BTreeMap<String, String>,
444 body: Vec<u8>,
445 content_length: Option<usize>,
446 chunked: bool,
447 chunk_remaining: usize,
449 chunk_size_parsed: bool,
451}
452
453#[cfg(feature = "alloc")]
454impl Default for ResponseParser {
455 fn default() -> Self {
456 Self::new()
457 }
458}
459
460#[cfg(feature = "alloc")]
461impl ResponseParser {
462 pub fn new() -> Self {
464 ResponseParser {
465 state: ParseState::StatusLine,
466 buffer: Vec::new(),
467 version: String::new(),
468 status_code: 0,
469 reason: String::new(),
470 headers: BTreeMap::new(),
471 body: Vec::new(),
472 content_length: None,
473 chunked: false,
474 chunk_remaining: 0,
475 chunk_size_parsed: false,
476 }
477 }
478
479 pub fn state(&self) -> &ParseState {
481 &self.state
482 }
483
484 pub fn feed(&mut self, data: &[u8]) -> Result<(), HttpError> {
486 self.buffer.extend_from_slice(data);
487
488 loop {
489 match self.state {
490 ParseState::StatusLine => {
491 if !self.try_parse_status_line()? {
492 return Ok(());
493 }
494 }
495 ParseState::Headers => {
496 if !self.try_parse_headers()? {
497 return Ok(());
498 }
499 }
500 ParseState::Body => {
501 self.try_parse_body();
502 return Ok(());
503 }
504 ParseState::ChunkedBody => {
505 if !self.try_parse_chunked()? {
506 return Ok(());
507 }
508 }
509 ParseState::Complete => return Ok(()),
510 }
511 }
512 }
513
514 fn try_parse_status_line(&mut self) -> Result<bool, HttpError> {
517 let line_end = match find_crlf(&self.buffer) {
518 Some(pos) => pos,
519 None => return Ok(false),
520 };
521
522 let line = match core::str::from_utf8(&self.buffer[..line_end]) {
523 Ok(s) => s,
524 Err(_) => return Err(HttpError::InvalidStatusLine),
525 };
526
527 let mut parts = line.splitn(3, ' ');
529 let version = parts.next().ok_or(HttpError::InvalidStatusLine)?;
530 let status_str = parts.next().ok_or(HttpError::InvalidStatusLine)?;
531 let reason = parts.next().unwrap_or("");
532
533 self.version = String::from(version);
534 self.status_code = parse_u16(status_str).map_err(|_| HttpError::InvalidStatusLine)?;
535 self.reason = String::from(reason);
536
537 let new_start = line_end + 2;
539 self.buffer = self.buffer[new_start..].to_vec();
540 self.state = ParseState::Headers;
541 Ok(true)
542 }
543
544 fn try_parse_headers(&mut self) -> Result<bool, HttpError> {
547 loop {
548 let line_end = match find_crlf(&self.buffer) {
549 Some(pos) => pos,
550 None => return Ok(false),
551 };
552
553 if line_end == 0 {
554 self.buffer = self.buffer[2..].to_vec();
556 self.determine_body_mode();
557 return Ok(true);
558 }
559
560 let line = match core::str::from_utf8(&self.buffer[..line_end]) {
561 Ok(s) => s,
562 Err(_) => return Err(HttpError::InvalidHeader),
563 };
564
565 if let Some(colon_pos) = line.find(':') {
566 let name = to_lowercase(&line[..colon_pos]);
567 let value = line[colon_pos + 1..].trim_start();
568 self.headers.insert(name, String::from(value));
569 }
570
571 let new_start = line_end + 2;
572 self.buffer = self.buffer[new_start..].to_vec();
573 }
574 }
575
576 fn determine_body_mode(&mut self) {
578 if let Some(te) = self.headers.get("transfer-encoding") {
580 if to_lowercase(te).contains("chunked") {
581 self.chunked = true;
582 self.state = ParseState::ChunkedBody;
583 return;
584 }
585 }
586
587 if let Some(cl) = self.headers.get("content-length") {
589 if let Ok(len) = parse_usize(cl) {
590 self.content_length = Some(len);
591 if len == 0 {
592 self.state = ParseState::Complete;
593 } else {
594 self.state = ParseState::Body;
595 }
596 return;
597 }
598 }
599
600 self.state = ParseState::Complete;
602 }
603
604 fn try_parse_body(&mut self) {
606 if let Some(expected) = self.content_length {
607 if self.buffer.len() >= expected {
608 self.body = self.buffer[..expected].to_vec();
609 self.buffer = self.buffer[expected..].to_vec();
610 self.state = ParseState::Complete;
611 }
612 }
614 }
615
616 fn try_parse_chunked(&mut self) -> Result<bool, HttpError> {
619 loop {
620 if !self.chunk_size_parsed {
621 let line_end = match find_crlf(&self.buffer) {
623 Some(pos) => pos,
624 None => return Ok(false),
625 };
626
627 let size_line = match core::str::from_utf8(&self.buffer[..line_end]) {
628 Ok(s) => s,
629 Err(_) => return Err(HttpError::InvalidChunkSize),
630 };
631
632 let size_str = match size_line.find(';') {
634 Some(idx) => &size_line[..idx],
635 None => size_line,
636 };
637
638 let chunk_size =
639 parse_hex_usize(size_str.trim()).map_err(|_| HttpError::InvalidChunkSize)?;
640
641 self.buffer = self.buffer[line_end + 2..].to_vec();
642
643 if chunk_size == 0 {
644 if self.buffer.len() >= 2 && self.buffer[0] == b'\r' && self.buffer[1] == b'\n'
646 {
647 self.buffer = self.buffer[2..].to_vec();
648 }
649 self.state = ParseState::Complete;
650 return Ok(true);
651 }
652
653 self.chunk_remaining = chunk_size;
654 self.chunk_size_parsed = true;
655 }
656
657 if self.buffer.len() < self.chunk_remaining {
659 return Ok(false);
660 }
661
662 self.body
663 .extend_from_slice(&self.buffer[..self.chunk_remaining]);
664 self.buffer = self.buffer[self.chunk_remaining..].to_vec();
665 self.chunk_remaining = 0;
666 self.chunk_size_parsed = false;
667
668 if self.buffer.len() >= 2 && self.buffer[0] == b'\r' && self.buffer[1] == b'\n' {
670 self.buffer = self.buffer[2..].to_vec();
671 } else if self.buffer.len() < 2 {
672 return Ok(false);
673 }
674 }
675 }
676
677 pub fn take_response(&mut self) -> Option<HttpResponse> {
680 if self.state != ParseState::Complete {
681 return None;
682 }
683
684 Some(HttpResponse {
685 version: core::mem::take(&mut self.version),
686 status_code: self.status_code,
687 reason: core::mem::take(&mut self.reason),
688 headers: core::mem::take(&mut self.headers),
689 body: core::mem::take(&mut self.body),
690 })
691 }
692}
693
694#[cfg(feature = "alloc")]
704pub struct HttpClient {
705 pub default_headers: BTreeMap<String, String>,
707 pub timeout_ms: u64,
709 pub max_redirects: u8,
711}
712
713#[cfg(feature = "alloc")]
714impl Default for HttpClient {
715 fn default() -> Self {
716 Self::new()
717 }
718}
719
720#[cfg(feature = "alloc")]
721impl HttpClient {
722 pub fn new() -> Self {
724 let mut default_headers = BTreeMap::new();
725 default_headers.insert(String::from("user-agent"), String::from(DEFAULT_USER_AGENT));
726 default_headers.insert(String::from("accept"), String::from("*/*"));
727
728 HttpClient {
729 default_headers,
730 timeout_ms: DEFAULT_TIMEOUT_MS,
731 max_redirects: DEFAULT_MAX_REDIRECTS,
732 }
733 }
734
735 pub fn set_default_header(&mut self, name: &str, value: &str) {
737 self.default_headers
738 .insert(to_lowercase(name), String::from(value));
739 }
740
741 pub fn prepare_request(&self, request: &mut HttpRequest) -> Vec<u8> {
743 for (name, value) in &self.default_headers {
745 if !request.headers.contains_key(name) {
746 request.headers.insert(name.clone(), value.clone());
747 }
748 }
749 request.serialize()
750 }
751
752 pub fn create_parser(&self) -> ResponseParser {
754 ResponseParser::new()
755 }
756
757 pub fn get(&self, url: &str) -> Result<Vec<u8>, HttpError> {
759 let mut req = HttpRequest::new(HttpMethod::Get, url)?;
760 Ok(self.prepare_request(&mut req))
761 }
762
763 pub fn post(&self, url: &str, body: &[u8], content_type: &str) -> Result<Vec<u8>, HttpError> {
765 let mut req = HttpRequest::new(HttpMethod::Post, url)?;
766 req.set_content_type(content_type);
767 req.set_body(Vec::from(body));
768 Ok(self.prepare_request(&mut req))
769 }
770
771 pub fn follow_redirect(
774 &self,
775 response: &HttpResponse,
776 original_url: &str,
777 remaining_redirects: &mut u8,
778 ) -> Result<Option<HttpRequest>, HttpError> {
779 if !response.is_redirect() {
780 return Ok(None);
781 }
782
783 if *remaining_redirects == 0 {
784 return Err(HttpError::TooManyRedirects);
785 }
786 *remaining_redirects -= 1;
787
788 let location = match response.redirect_location() {
789 Some(loc) => loc.clone(),
790 None => return Ok(None),
791 };
792
793 let full_url = if location.starts_with("http://") || location.starts_with("https://") {
795 location
796 } else {
797 let orig = ParsedUrl::parse(original_url)?;
798 let scheme = &orig.scheme;
799 if orig.port != HTTP_PORT && orig.port != HTTPS_PORT {
800 format!("{}://{}:{}{}", scheme, orig.host, orig.port, location)
801 } else {
802 format!("{}://{}{}", scheme, orig.host, location)
803 }
804 };
805
806 let method = HttpMethod::Get;
809
810 let req = HttpRequest::new(method, &full_url)?;
811 Ok(Some(req))
812 }
813}
814
815#[cfg(feature = "alloc")]
824pub fn percent_encode(input: &str) -> String {
825 let mut encoded = String::with_capacity(input.len());
826 for byte in input.bytes() {
827 if is_unreserved(byte) {
828 encoded.push(byte as char);
829 } else if byte == b' ' {
830 encoded.push('+');
831 } else {
832 encoded.push('%');
833 encoded.push(hex_digit(byte >> 4));
834 encoded.push(hex_digit(byte & 0x0F));
835 }
836 }
837 encoded
838}
839
840#[cfg(feature = "alloc")]
844pub fn encode_query_string(params: &[(&str, &str)]) -> String {
845 let mut result = String::new();
846 for (i, (key, value)) in params.iter().enumerate() {
847 if i > 0 {
848 result.push('&');
849 }
850 result.push_str(&percent_encode(key));
851 result.push('=');
852 result.push_str(&percent_encode(value));
853 }
854 result
855}
856
857#[cfg(feature = "alloc")]
861pub fn basic_auth_header(user: &str, password: &str) -> String {
862 let credentials = format!("{}:{}", user, password);
863 let encoded = base64_encode(credentials.as_bytes());
864 format!("Basic {}", encoded)
865}
866
867#[cfg(feature = "alloc")]
869pub fn build_cookie_header(cookies: &[(&str, &str)]) -> String {
870 let mut result = String::new();
871 for (i, (name, value)) in cookies.iter().enumerate() {
872 if i > 0 {
873 result.push_str("; ");
874 }
875 result.push_str(name);
876 result.push('=');
877 result.push_str(value);
878 }
879 result
880}
881
882fn find_crlf(data: &[u8]) -> Option<usize> {
888 if data.len() < 2 {
889 return None;
890 }
891 let limit = data.len() - 1;
892 let mut i = 0;
893 while i < limit {
894 if data[i] == b'\r' && data[i + 1] == b'\n' {
895 return Some(i);
896 }
897 i += 1;
898 }
899 None
900}
901
902#[cfg(feature = "alloc")]
904fn to_lowercase(s: &str) -> String {
905 let mut result = String::with_capacity(s.len());
906 for c in s.chars() {
907 if c.is_ascii_uppercase() {
908 result.push((c as u8 + 32) as char);
909 } else {
910 result.push(c);
911 }
912 }
913 result
914}
915
916fn parse_u16(s: &str) -> Result<u16, ()> {
918 let mut val: u16 = 0;
919 for byte in s.bytes() {
920 if !byte.is_ascii_digit() {
921 return Err(());
922 }
923 val = val.checked_mul(10).ok_or(())?;
924 val = val.checked_add((byte - b'0') as u16).ok_or(())?;
925 }
926 Ok(val)
927}
928
929fn parse_usize(s: &str) -> Result<usize, ()> {
931 let s = s.trim();
932 let mut val: usize = 0;
933 for byte in s.bytes() {
934 if !byte.is_ascii_digit() {
935 return Err(());
936 }
937 val = val.checked_mul(10).ok_or(())?;
938 val = val.checked_add((byte - b'0') as usize).ok_or(())?;
939 }
940 Ok(val)
941}
942
943fn parse_hex_usize(s: &str) -> Result<usize, ()> {
945 let mut val: usize = 0;
946 for byte in s.bytes() {
947 let digit = match byte {
948 b'0'..=b'9' => (byte - b'0') as usize,
949 b'a'..=b'f' => (byte - b'a') as usize + 10,
950 b'A'..=b'F' => (byte - b'A') as usize + 10,
951 _ => return Err(()),
952 };
953 val = val.checked_mul(16).ok_or(())?;
954 val = val.checked_add(digit).ok_or(())?;
955 }
956 Ok(val)
957}
958
959#[cfg(feature = "alloc")]
961fn uint_to_string(mut n: usize) -> String {
962 if n == 0 {
963 return String::from("0");
964 }
965 let mut digits = Vec::new();
966 while n > 0 {
967 digits.push(b'0' + (n % 10) as u8);
968 n /= 10;
969 }
970 digits.reverse();
971 String::from_utf8(digits).unwrap_or_else(|_| String::from("0"))
973}
974
975fn is_unreserved(b: u8) -> bool {
977 matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~')
978}
979
980fn hex_digit(val: u8) -> char {
982 let nibble = val & 0x0F;
983 if nibble < 10 {
984 (b'0' + nibble) as char
985 } else {
986 (b'A' + nibble - 10) as char
987 }
988}
989
990#[cfg(feature = "alloc")]
992fn base64_encode(data: &[u8]) -> String {
993 const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
994
995 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
996 let mut i = 0;
997
998 while i + 2 < data.len() {
999 let n = ((data[i] as u32) << 16) | ((data[i + 1] as u32) << 8) | (data[i + 2] as u32);
1000 result.push(TABLE[((n >> 18) & 0x3F) as usize] as char);
1001 result.push(TABLE[((n >> 12) & 0x3F) as usize] as char);
1002 result.push(TABLE[((n >> 6) & 0x3F) as usize] as char);
1003 result.push(TABLE[(n & 0x3F) as usize] as char);
1004 i += 3;
1005 }
1006
1007 let remaining = data.len() - i;
1008 if remaining == 1 {
1009 let n = (data[i] as u32) << 16;
1010 result.push(TABLE[((n >> 18) & 0x3F) as usize] as char);
1011 result.push(TABLE[((n >> 12) & 0x3F) as usize] as char);
1012 result.push('=');
1013 result.push('=');
1014 } else if remaining == 2 {
1015 let n = ((data[i] as u32) << 16) | ((data[i + 1] as u32) << 8);
1016 result.push(TABLE[((n >> 18) & 0x3F) as usize] as char);
1017 result.push(TABLE[((n >> 12) & 0x3F) as usize] as char);
1018 result.push(TABLE[((n >> 6) & 0x3F) as usize] as char);
1019 result.push('=');
1020 }
1021
1022 result
1023}
1024
1025#[cfg(test)]
1030mod tests {
1031 #[allow(unused_imports)]
1032 use alloc::vec;
1033
1034 use super::*;
1035
1036 #[test]
1039 fn test_url_parse_http() {
1040 let url = ParsedUrl::parse("http://example.com").unwrap();
1041 assert_eq!(url.scheme, "http");
1042 assert_eq!(url.host, "example.com");
1043 assert_eq!(url.port, 80);
1044 assert_eq!(url.path, "/");
1045 assert_eq!(url.query, None);
1046 assert!(!url.is_https);
1047 }
1048
1049 #[test]
1050 fn test_url_parse_https() {
1051 let url = ParsedUrl::parse("https://secure.example.com").unwrap();
1052 assert_eq!(url.scheme, "https");
1053 assert_eq!(url.host, "secure.example.com");
1054 assert_eq!(url.port, 443);
1055 assert!(url.is_https);
1056 }
1057
1058 #[test]
1059 fn test_url_parse_with_port() {
1060 let url = ParsedUrl::parse("http://localhost:8080/api").unwrap();
1061 assert_eq!(url.host, "localhost");
1062 assert_eq!(url.port, 8080);
1063 assert_eq!(url.path, "/api");
1064 }
1065
1066 #[test]
1067 fn test_url_parse_with_path() {
1068 let url = ParsedUrl::parse("http://example.com/path/to/resource").unwrap();
1069 assert_eq!(url.path, "/path/to/resource");
1070 }
1071
1072 #[test]
1073 fn test_url_parse_with_query() {
1074 let url = ParsedUrl::parse("http://example.com/search?q=rust&page=1").unwrap();
1075 assert_eq!(url.path, "/search");
1076 assert_eq!(url.query, Some(String::from("q=rust&page=1")));
1077 assert_eq!(url.request_path(), "/search?q=rust&page=1");
1078 }
1079
1080 #[test]
1081 fn test_url_parse_invalid() {
1082 assert_eq!(
1083 ParsedUrl::parse("ftp://example.com"),
1084 Err(HttpError::InvalidUrl)
1085 );
1086 }
1087
1088 #[test]
1091 fn test_request_get_serialization() {
1092 let req = HttpRequest::new(HttpMethod::Get, "http://example.com/index.html").unwrap();
1093 let bytes = req.serialize();
1094 let text = core::str::from_utf8(&bytes).unwrap();
1095
1096 assert!(text.starts_with("GET /index.html HTTP/1.1\r\n"));
1097 assert!(text.contains("host: example.com\r\n"));
1098 assert!(text.ends_with("\r\n\r\n"));
1099 }
1100
1101 #[test]
1102 fn test_request_post_with_body() {
1103 let mut req = HttpRequest::new(HttpMethod::Post, "http://example.com/api").unwrap();
1104 req.set_content_type(MIME_APPLICATION_JSON);
1105 req.set_body_str("{\"key\":\"value\"}");
1106 let bytes = req.serialize();
1107 let text = core::str::from_utf8(&bytes).unwrap();
1108
1109 assert!(text.starts_with("POST /api HTTP/1.1\r\n"));
1110 assert!(text.contains("content-type: application/json\r\n"));
1111 assert!(text.contains("content-length: 15\r\n"));
1112 assert!(text.ends_with("{\"key\":\"value\"}"));
1113 }
1114
1115 #[test]
1118 fn test_response_status_line() {
1119 let mut parser = ResponseParser::new();
1120 parser.feed(b"HTTP/1.1 200 OK\r\n\r\n").unwrap();
1121 let resp = parser.take_response().unwrap();
1122 assert_eq!(resp.version, "HTTP/1.1");
1123 assert_eq!(resp.status_code, 200);
1124 assert_eq!(resp.reason, "OK");
1125 }
1126
1127 #[test]
1130 fn test_response_single_header() {
1131 let mut parser = ResponseParser::new();
1132 parser
1133 .feed(b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n")
1134 .unwrap();
1135 let resp = parser.take_response().unwrap();
1136 assert_eq!(
1137 resp.headers.get("content-type"),
1138 Some(&String::from("text/html"))
1139 );
1140 }
1141
1142 #[test]
1143 fn test_response_multiple_headers() {
1144 let mut parser = ResponseParser::new();
1145 parser
1146 .feed(b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 5\r\n\r\nhello")
1147 .unwrap();
1148 let resp = parser.take_response().unwrap();
1149 assert_eq!(
1150 resp.headers.get("content-type"),
1151 Some(&String::from("text/html"))
1152 );
1153 assert_eq!(resp.headers.get("content-length"), Some(&String::from("5")));
1154 assert_eq!(resp.body, b"hello");
1155 }
1156
1157 #[test]
1158 fn test_response_case_insensitive_headers() {
1159 let mut parser = ResponseParser::new();
1160 parser
1161 .feed(b"HTTP/1.1 200 OK\r\nCONTENT-TYPE: text/plain\r\n\r\n")
1162 .unwrap();
1163 let resp = parser.take_response().unwrap();
1164 assert_eq!(
1165 resp.headers.get("content-type"),
1166 Some(&String::from("text/plain"))
1167 );
1168 }
1169
1170 #[test]
1173 fn test_content_length_body() {
1174 let mut parser = ResponseParser::new();
1175 parser
1176 .feed(b"HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHello World")
1177 .unwrap();
1178 let resp = parser.take_response().unwrap();
1179 assert_eq!(resp.body, b"Hello World");
1180 assert_eq!(resp.body_as_str(), Some("Hello World"));
1181 }
1182
1183 #[test]
1184 fn test_chunked_transfer_decoding() {
1185 let mut parser = ResponseParser::new();
1186 parser
1187 .feed(
1188 b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n\
1189 5\r\nHello\r\n6\r\n World\r\n0\r\n\r\n",
1190 )
1191 .unwrap();
1192 assert_eq!(parser.state(), &ParseState::Complete);
1193 let resp = parser.take_response().unwrap();
1194 assert_eq!(resp.body, b"Hello World");
1195 }
1196
1197 #[test]
1200 fn test_redirect_301() {
1201 let mut parser = ResponseParser::new();
1202 parser
1203 .feed(b"HTTP/1.1 301 Moved Permanently\r\nLocation: http://new.example.com/\r\n\r\n")
1204 .unwrap();
1205 let resp = parser.take_response().unwrap();
1206 assert!(resp.is_redirect());
1207 assert_eq!(
1208 resp.redirect_location(),
1209 Some(&String::from("http://new.example.com/"))
1210 );
1211 }
1212
1213 #[test]
1214 fn test_redirect_302() {
1215 let mut parser = ResponseParser::new();
1216 parser
1217 .feed(b"HTTP/1.1 302 Found\r\nLocation: /new-path\r\n\r\n")
1218 .unwrap();
1219 let resp = parser.take_response().unwrap();
1220 assert!(resp.is_redirect());
1221 assert_eq!(resp.status_code, 302);
1222 }
1223
1224 #[test]
1225 fn test_redirect_307() {
1226 let mut parser = ResponseParser::new();
1227 parser
1228 .feed(b"HTTP/1.1 307 Temporary Redirect\r\nLocation: /temp\r\n\r\n")
1229 .unwrap();
1230 let resp = parser.take_response().unwrap();
1231 assert!(resp.is_redirect());
1232 assert_eq!(resp.status_code, 307);
1233 }
1234
1235 #[test]
1238 fn test_query_string_encoding() {
1239 let qs = encode_query_string(&[("key", "value"), ("name", "hello world")]);
1240 assert_eq!(qs, "key=value&name=hello+world");
1241 }
1242
1243 #[test]
1244 fn test_percent_encode_special_chars() {
1245 let encoded = percent_encode("a&b=c d");
1246 assert_eq!(encoded, "a%26b%3Dc+d");
1247 }
1248
1249 #[test]
1252 fn test_keep_alive_detection() {
1253 let mut parser = ResponseParser::new();
1254 parser
1255 .feed(b"HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n")
1256 .unwrap();
1257 let resp = parser.take_response().unwrap();
1258 assert!(resp.is_keep_alive());
1259 }
1260
1261 #[test]
1262 fn test_http11_default_keep_alive() {
1263 let mut parser = ResponseParser::new();
1264 parser
1265 .feed(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
1266 .unwrap();
1267 let resp = parser.take_response().unwrap();
1268 assert!(resp.is_keep_alive());
1270 }
1271
1272 #[test]
1275 fn test_basic_auth_header() {
1276 let header = basic_auth_header("user", "pass");
1277 assert_eq!(header, "Basic dXNlcjpwYXNz");
1279 }
1280
1281 #[test]
1284 fn test_cookie_header_construction() {
1285 let cookie = build_cookie_header(&[("session", "abc123"), ("lang", "en")]);
1286 assert_eq!(cookie, "session=abc123; lang=en");
1287 }
1288
1289 #[test]
1292 fn test_method_as_str() {
1293 assert_eq!(HttpMethod::Get.as_str(), "GET");
1294 assert_eq!(HttpMethod::Post.as_str(), "POST");
1295 assert_eq!(HttpMethod::Put.as_str(), "PUT");
1296 assert_eq!(HttpMethod::Delete.as_str(), "DELETE");
1297 assert_eq!(HttpMethod::Head.as_str(), "HEAD");
1298 assert_eq!(HttpMethod::Patch.as_str(), "PATCH");
1299 assert_eq!(HttpMethod::Options.as_str(), "OPTIONS");
1300 }
1301
1302 #[test]
1305 fn test_empty_response_no_body() {
1306 let mut parser = ResponseParser::new();
1307 parser.feed(b"HTTP/1.1 204 No Content\r\n\r\n").unwrap();
1308 let resp = parser.take_response().unwrap();
1309 assert_eq!(resp.status_code, 204);
1310 assert!(resp.body.is_empty());
1311 }
1312
1313 #[test]
1316 fn test_incremental_parsing() {
1317 let mut parser = ResponseParser::new();
1318
1319 parser.feed(b"HTTP/1.1 200 OK\r\n").unwrap();
1321 assert_eq!(parser.state(), &ParseState::Headers);
1322
1323 parser.feed(b"Content-Length: 5\r\n\r\n").unwrap();
1325 assert_eq!(parser.state(), &ParseState::Body);
1326
1327 parser.feed(b"Hel").unwrap();
1329 assert_eq!(parser.state(), &ParseState::Body);
1330
1331 parser.feed(b"lo").unwrap();
1333 assert_eq!(parser.state(), &ParseState::Complete);
1334
1335 let resp = parser.take_response().unwrap();
1336 assert_eq!(resp.body, b"Hello");
1337 }
1338
1339 #[test]
1342 fn test_base64_encode() {
1343 assert_eq!(base64_encode(b""), "");
1344 assert_eq!(base64_encode(b"f"), "Zg==");
1345 assert_eq!(base64_encode(b"fo"), "Zm8=");
1346 assert_eq!(base64_encode(b"foo"), "Zm9v");
1347 assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
1348 }
1349
1350 #[test]
1353 fn test_parse_hex_usize() {
1354 assert_eq!(parse_hex_usize("0"), Ok(0));
1355 assert_eq!(parse_hex_usize("5"), Ok(5));
1356 assert_eq!(parse_hex_usize("a"), Ok(10));
1357 assert_eq!(parse_hex_usize("1F"), Ok(31));
1358 assert_eq!(parse_hex_usize("ff"), Ok(255));
1359 }
1360}