lychee_lib/types/
cookies.rs

1use crate::{ErrorKind, Result};
2use log::info;
3use reqwest_cookie_store::{CookieStore as ReqwestCookieStore, CookieStoreMutex};
4use std::io::ErrorKind as IoErrorKind;
5use std::{path::PathBuf, sync::Arc};
6
7/// A wrapper around `reqwest_cookie_store::CookieStore`
8///
9/// We keep track of the file path of the cookie store and
10/// implement `PartialEq` to compare cookie jars by their path
11#[derive(Debug, Clone)]
12pub struct CookieJar {
13    pub(crate) path: PathBuf,
14    pub(crate) inner: Arc<CookieStoreMutex>,
15}
16
17impl CookieJar {
18    /// Load a cookie store from a file
19    ///
20    /// Currently only JSON files are supported
21    ///
22    /// # Errors
23    ///
24    /// This function will return an error if
25    /// - the file cannot be opened (except for `NotFound`) or
26    /// - if the file is not valid JSON in either new or legacy format
27    pub fn load(path: PathBuf) -> Result<Self> {
28        match std::fs::File::open(&path).map(std::io::BufReader::new) {
29            Ok(mut reader) => {
30                info!("Loading cookies from {}", path.display());
31
32                // Try loading with new format first, fall back to legacy format
33                #[allow(clippy::single_match_else)]
34                let store = match cookie_store::serde::json::load(&mut reader) {
35                    Ok(store) => store,
36                    Err(_) => {
37                        // Reopen file for legacy format attempt
38                        let reader = std::fs::File::open(&path).map(std::io::BufReader::new)?;
39                        #[allow(deprecated)]
40                        ReqwestCookieStore::load_json(reader).map_err(|e| {
41                            ErrorKind::Cookies(format!("Failed to load cookies: {e}"))
42                        })?
43                    }
44                };
45
46                Ok(Self {
47                    path,
48                    inner: Arc::new(CookieStoreMutex::new(store)),
49                })
50            }
51            // Create a new cookie store if the file does not exist
52            Err(e) if e.kind() == IoErrorKind::NotFound => Ok(Self {
53                path,
54                inner: Arc::new(CookieStoreMutex::new(ReqwestCookieStore::default())),
55            }),
56            // Propagate other IO errors (like permission denied) to the caller
57            Err(e) => Err(e.into()),
58        }
59    }
60
61    /// Save the cookie store to file as JSON
62    /// This will overwrite the file, which was loaded if any
63    ///
64    /// # Errors
65    ///
66    /// This function will return an error if
67    /// - the cookie store is locked or
68    /// - the file cannot be opened or
69    /// - if the file cannot be written to or
70    /// - if the file cannot be serialized to JSON
71    pub fn save(&self) -> Result<()> {
72        info!("Saving cookies to {}", self.path.display());
73        // Create parent directories if they don't exist
74        if let Some(parent) = self.path.parent() {
75            std::fs::create_dir_all(parent)?;
76        }
77        let mut file = std::fs::File::create(&self.path)?;
78        let store = self
79            .inner
80            .lock()
81            .map_err(|e| ErrorKind::Cookies(format!("Failed to lock cookie store: {e}")))?;
82        cookie_store::serde::json::save(&store, &mut file)
83            .map_err(|e| ErrorKind::Cookies(format!("Failed to save cookies: {e}")))
84    }
85}
86
87impl std::ops::Deref for CookieJar {
88    type Target = Arc<CookieStoreMutex>;
89    fn deref(&self) -> &Self::Target {
90        &self.inner
91    }
92}
93
94impl PartialEq for CookieJar {
95    fn eq(&self, other: &Self) -> bool {
96        // Assume that the cookie jar is the same if the path is the same
97        // Comparing the cookie stores directly is not possible because the
98        // `CookieStore` struct does not implement `Eq`
99        self.path == other.path
100    }
101}