1use std::fs;
5use std::path::{Path, PathBuf};
6
7use crate::error::{DesktopError, Result};
8use crate::{DesktopResponseContent, DesktopResponseFile};
9
10#[cfg(feature = "application-ipc")]
12pub const IPC_ENDPOINT: &str = "/_webui/ipc";
13
14pub const DEFAULT_MAX_ASSET_BYTES: u64 = 32 * 1024 * 1024;
16
17pub const DEFAULT_MAX_REQUEST_BYTES: u64 = 1024 * 1024;
19
20#[derive(Clone, Debug, Eq, PartialEq)]
22pub enum DesktopHttpMethod {
23 Get,
25 Post,
27 Other(String),
29}
30
31impl DesktopHttpMethod {
32 #[must_use]
34 pub fn parse(method: &str) -> Self {
35 match method {
36 "GET" | "get" => Self::Get,
37 "POST" | "post" => Self::Post,
38 other => Self::Other(other.to_string()),
39 }
40 }
41}
42
43pub struct DesktopProtocolRequest<'a> {
45 pub method: DesktopHttpMethod,
47 pub path: &'a str,
49 pub body: &'a [u8],
51 pub wants_json: bool,
53}
54
55impl<'a> DesktopProtocolRequest<'a> {
56 #[must_use]
58 pub fn get(path: &'a str) -> Self {
59 Self {
60 method: DesktopHttpMethod::Get,
61 path,
62 body: &[],
63 wants_json: false,
64 }
65 }
66
67 #[must_use]
69 pub fn post(path: &'a str, body: &'a [u8]) -> Self {
70 Self {
71 method: DesktopHttpMethod::Post,
72 path,
73 body,
74 wants_json: false,
75 }
76 }
77}
78
79#[derive(Debug)]
81pub struct DesktopProtocolResponse {
82 pub status: u16,
84 pub content_type: String,
86 pub body: DesktopResponseContent,
88}
89
90pub struct DesktopResponseBody {
95 bytes: Vec<u8>,
96 lease: Option<DesktopResponseLease>,
97}
98
99pub struct DesktopResponseLease {
101 _owner: Box<dyn Send + Sync>,
102}
103
104impl DesktopResponseBody {
105 #[must_use]
107 pub fn with_guard<G: Send + Sync + 'static>(bytes: Vec<u8>, guard: G) -> Self {
108 Self {
109 bytes,
110 lease: Some(DesktopResponseLease {
111 _owner: Box::new(guard),
112 }),
113 }
114 }
115
116 #[must_use]
118 pub fn as_slice(&self) -> &[u8] {
119 &self.bytes
120 }
121
122 #[must_use]
127 pub fn into_parts(self) -> (Vec<u8>, Option<DesktopResponseLease>) {
128 (self.bytes, self.lease)
129 }
130}
131
132impl From<Vec<u8>> for DesktopResponseBody {
133 fn from(bytes: Vec<u8>) -> Self {
134 Self { bytes, lease: None }
135 }
136}
137
138impl AsRef<[u8]> for DesktopResponseBody {
139 fn as_ref(&self) -> &[u8] {
140 &self.bytes
141 }
142}
143
144impl std::ops::Deref for DesktopResponseBody {
145 type Target = [u8];
146
147 fn deref(&self) -> &Self::Target {
148 &self.bytes
149 }
150}
151
152impl std::fmt::Debug for DesktopResponseBody {
153 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 formatter
155 .debug_struct("DesktopResponseBody")
156 .field("length", &self.bytes.len())
157 .field("reserved", &self.lease.is_some())
158 .finish()
159 }
160}
161
162impl<T: AsRef<[u8]> + ?Sized> PartialEq<T> for DesktopResponseBody {
163 fn eq(&self, other: &T) -> bool {
164 self.bytes.as_slice() == other.as_ref()
165 }
166}
167
168impl Eq for DesktopResponseBody {}
169
170impl DesktopProtocolResponse {
171 #[must_use]
173 pub fn new(
174 status: u16,
175 content_type: impl Into<String>,
176 body: impl Into<DesktopResponseContent>,
177 ) -> Self {
178 Self {
179 status,
180 content_type: content_type.into(),
181 body: body.into(),
182 }
183 }
184
185 #[must_use]
187 pub fn text(status: u16, body: impl Into<String>) -> Self {
188 Self::new(
189 status,
190 "text/plain; charset=utf-8",
191 body.into().into_bytes(),
192 )
193 }
194
195 #[must_use]
197 pub fn protobuf(body: impl Into<DesktopResponseBody>) -> Self {
198 Self::new(200, "application/x-protobuf", body.into())
199 }
200
201 #[must_use]
203 pub fn html(body: impl Into<Vec<u8>>) -> Self {
204 Self::new(200, "text/html; charset=utf-8", body.into())
205 }
206}
207
208pub(crate) fn read_asset_response(
209 asset_root: &Path,
210 asset_path: PathBuf,
211 max_asset_bytes: u64,
212) -> Result<Option<DesktopProtocolResponse>> {
213 let canonical = match asset_path.canonicalize() {
214 Ok(path) => path,
215 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
216 Err(source) => {
217 return Err(DesktopError::Io {
218 context: format!("resolving desktop asset {}", asset_path.display()),
219 source,
220 })
221 }
222 };
223
224 if !canonical.starts_with(asset_root) {
225 return Err(DesktopError::InvalidAssetPath {
226 path: canonical.display().to_string(),
227 });
228 }
229
230 let metadata = fs::metadata(&canonical).map_err(|source| DesktopError::Io {
231 context: format!("reading metadata for desktop asset {}", canonical.display()),
232 source,
233 })?;
234
235 if !metadata.is_file() {
236 return Ok(None);
237 }
238
239 let size = metadata.len();
240 if size > max_asset_bytes {
241 return Err(DesktopError::AssetTooLarge {
242 path: canonical,
243 size,
244 max_bytes: max_asset_bytes,
245 });
246 }
247
248 let content_type = mime_guess::from_path(&canonical)
249 .first_or_octet_stream()
250 .to_string();
251
252 read_known_asset_response(asset_root, &canonical, &content_type, size, max_asset_bytes)
253 .map(Some)
254}
255
256pub(crate) fn read_known_asset_response(
257 asset_root: &Path,
258 asset_path: &Path,
259 content_type: &str,
260 size_bytes: u64,
261 max_asset_bytes: u64,
262) -> Result<DesktopProtocolResponse> {
263 if size_bytes > max_asset_bytes {
264 return Err(DesktopError::AssetTooLarge {
265 path: asset_path.to_path_buf(),
266 size: size_bytes,
267 max_bytes: max_asset_bytes,
268 });
269 }
270
271 let file = crate::asset_file::open(asset_root, asset_path)?;
272 let metadata = file.metadata().map_err(|source| DesktopError::Io {
273 context: format!("checking opened desktop asset {}", asset_path.display()),
274 source,
275 })?;
276 if !metadata.is_file() {
277 return Err(DesktopError::InvalidAssetPath {
278 path: asset_path.display().to_string(),
279 });
280 }
281 if metadata.len() > max_asset_bytes {
282 return Err(DesktopError::AssetTooLarge {
283 path: asset_path.to_path_buf(),
284 size: metadata.len(),
285 max_bytes: max_asset_bytes,
286 });
287 }
288
289 Ok(DesktopProtocolResponse::new(
290 200,
291 content_type.to_string(),
292 DesktopResponseContent::File(DesktopResponseFile::new(file, metadata.len())),
293 ))
294}
295
296#[cfg(test)]
297#[allow(clippy::disallowed_methods)]
298mod response_body_tests {
299 use super::*;
300 use std::sync::{
301 atomic::{AtomicUsize, Ordering},
302 Arc,
303 };
304
305 struct ReleaseCount(Arc<AtomicUsize>);
306
307 impl Drop for ReleaseCount {
308 fn drop(&mut self) {
309 self.0.fetch_add(1, Ordering::SeqCst);
310 }
311 }
312
313 #[test]
314 fn response_body_retains_its_reservation_until_drop() {
315 let released = Arc::new(AtomicUsize::new(0));
316 let bytes = vec![1, 2, 3];
317 let address = bytes.as_ptr();
318 let body = DesktopResponseBody::with_guard(bytes, ReleaseCount(Arc::clone(&released)));
319 assert_eq!(body.as_slice(), &[1, 2, 3]);
320 assert_eq!(body.as_ptr(), address);
321 assert_eq!(released.load(Ordering::SeqCst), 0);
322 drop(body);
323 assert_eq!(released.load(Ordering::SeqCst), 1);
324 }
325
326 #[test]
327 fn native_transfer_preserves_reservation_and_allocation() {
328 let released = Arc::new(AtomicUsize::new(0));
329 let bytes = vec![4, 5, 6];
330 let address = bytes.as_ptr();
331 let response = DesktopProtocolResponse::protobuf(DesktopResponseBody::with_guard(
332 bytes,
333 ReleaseCount(Arc::clone(&released)),
334 ));
335 let (bytes, lease) = response.body.into_bytes().unwrap().into_parts();
336 assert_eq!(bytes.as_ptr(), address);
337 assert_eq!(released.load(Ordering::SeqCst), 0);
338 drop(bytes);
339 assert_eq!(released.load(Ordering::SeqCst), 0);
340 drop(lease);
341 assert_eq!(released.load(Ordering::SeqCst), 1);
342 }
343
344 #[test]
345 fn ordinary_responses_do_not_need_a_reservation() {
346 let response = DesktopProtocolResponse::new(201, "text/plain", b"created".to_vec());
347 assert_eq!(response.body, b"created");
348 let (bytes, lease) = response.body.into_bytes().unwrap().into_parts();
349 assert_eq!(bytes, b"created");
350 assert!(lease.is_none());
351 }
352}