🦠 The Definitive Gemini Protocol Toolkit
gemini gemini-protocol gemtext parser zero-dependency toolkit ast converter html markdown cli networking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(response): Expose content bytes

Fuwn 878978d9 fe6f6638

+31 -27
+31 -27
src/request/response.rs
··· 16 16 // Copyright (C) 2022-2025 Fuwn <contact@fuwn.me> 17 17 // SPDX-License-Identifier: GPL-3.0-only 18 18 19 - use { 20 - crate::request::Status, 21 - rustls::SupportedCipherSuite, 22 - std::{borrow::Cow, fmt::Write}, 23 - }; 19 + use {crate::request::Status, rustls::SupportedCipherSuite, std::borrow::Cow}; 24 20 25 21 #[derive(Debug, Clone, PartialEq)] 26 22 pub struct Response { 27 23 status: Status, 28 24 meta: String, 29 - content: Option<String>, 25 + content: Option<Vec<u8>>, 30 26 size: usize, 31 27 suite: Option<SupportedCipherSuite>, 32 28 } 33 29 34 30 impl Response { 35 31 pub(crate) fn new(data: &[u8], suite: Option<SupportedCipherSuite>) -> Self { 36 - let string_form = String::from_utf8_lossy(data).to_string(); 37 - let mut content = None; 38 - let header; 39 - 40 - if string_form.ends_with("\r\n") { 41 - header = string_form; 32 + let delimiter = b"\r\n"; 33 + let header_end = data 34 + .windows(delimiter.len()) 35 + .position(|window| window == delimiter) 36 + .map_or(data.len(), |pos| pos + delimiter.len()); 37 + let header_bytes = &data[..header_end]; 38 + let header = String::from_utf8_lossy(header_bytes).trim_end().to_string(); 39 + let content_bytes = if header_end < data.len() { 40 + Some(data[header_end..].to_vec()) 42 41 } else { 43 - let mut string_split = string_form.split("\r\n"); 44 - 45 - header = string_split.next().unwrap_or("").to_string(); 46 - content = Some(string_split.fold(String::new(), |mut output, s| { 47 - let _ = write!(output, "{s}\r\n"); 48 - 49 - output 50 - })); 51 - } 52 - 53 - let header_split = header.split_at(2); 42 + None 43 + }; 44 + let (status_string, meta_string) = header.split_at(2); 45 + let status_code = status_string.parse::<i32>().unwrap_or(0); 54 46 55 47 Self { 56 - status: Status::from(header_split.0.parse::<i32>().unwrap_or(0)), 57 - meta: header_split.1.trim_start().to_string(), 58 - content, 48 + status: Status::from(status_code), 49 + meta: meta_string.trim_start().to_string(), 50 + content: content_bytes, 59 51 size: data.len(), 60 52 suite, 61 53 } ··· 68 60 #[must_use] 69 61 pub fn meta(&self) -> Cow<'_, str> { Cow::Borrowed(&self.meta) } 70 62 63 + /// This associated function assumes that the content is valid UTF-8. 64 + /// 65 + /// If you want to handle data bytes directly, use 66 + /// [`Response::content_bytes`]. 71 67 #[must_use] 72 - pub const fn content(&self) -> &Option<String> { &self.content } 68 + pub fn content(&self) -> Option<String> { 69 + self 70 + .content 71 + .as_ref() 72 + .map(|content| String::from_utf8_lossy(content).to_string()) 73 + } 74 + 75 + #[must_use] 76 + pub fn content_bytes(&self) -> Option<&[u8]> { self.content.as_deref() } 73 77 74 78 #[must_use] 75 79 pub const fn size(&self) -> &usize { &self.size }