cuprate_helper/
fs.rs

1//! Cuprate directories and filenames.
2//!
3//! # Environment variables on Linux
4//! Note that this module's functions uses [`dirs`],
5//! which adheres to the XDG standard on Linux.
6//!
7//! This means that the values returned by these statics
8//! may change at runtime depending on environment variables,
9//! for example:
10//!
11//! By default the config directory is `~/.config`, however
12//! if `$XDG_CONFIG_HOME` is set to something, that will be
13//! used instead.
14//!
15//! ```rust
16//! # use cuprate_helper::fs::*;
17//! # if cfg!(target_os = "linux") {
18//! std::env::set_var("XDG_CONFIG_HOME", "/custom/path");
19//! assert_eq!(
20//!     CUPRATE_CONFIG_DIR.to_string_lossy(),
21//!     "/custom/path/cuprate"
22//! );
23//! # }
24//! ```
25//!
26//! Reference:
27//! - <https://github.com/Cuprate/cuprate/issues/46>
28//! - <https://docs.rs/dirs>
29
30//---------------------------------------------------------------------------------------------------- Use
31use std::{
32    path::{Path, PathBuf},
33    sync::LazyLock,
34};
35
36use crate::network::Network;
37
38//---------------------------------------------------------------------------------------------------- Const
39/// Cuprate's main directory.
40///
41/// This is the head PATH node used for any top-level Cuprate directories.
42///
43/// | OS      | PATH                                                |
44/// |---------|-----------------------------------------------------|
45/// | Windows | `C:\Users\Alice\AppData\Roaming\Cuprate\`           |
46/// | macOS   | `/Users/Alice/Library/Application Support/Cuprate/` |
47/// | Linux   | `/home/alice/.config/cuprate/`                      |
48///
49/// This is shared between all Cuprate programs.
50///
51/// # Value
52/// This is `Cuprate` on `Windows|macOS` and `cuprate` on everything else.
53///
54/// # Monero Equivalent
55/// `.bitmonero`
56pub const CUPRATE_DIR: &str = {
57    if cfg!(target_os = "windows") || cfg!(target_os = "macos") {
58        // The standard for main directories is capitalized.
59        "Cuprate"
60    } else {
61        // Standard on Linux + BSDs is lowercase.
62        "cuprate"
63    }
64};
65
66/// The default name of Cuprate's config file.
67pub const DEFAULT_CONFIG_FILE_NAME: &str = "Cuprated.toml";
68
69//---------------------------------------------------------------------------------------------------- Directories
70/// Create a `LazyLock` for common PATHs used by Cuprate.
71///
72/// This currently creates these directories:
73/// - [`CUPRATE_CACHE_DIR`]
74/// - [`CUPRATE_CONFIG_DIR`]
75/// - [`CUPRATE_DATA_DIR`]
76/// - [`CUPRATE_BLOCKCHAIN_DIR`]
77macro_rules! impl_path_lazylock {
78    ($(
79        $(#[$attr:meta])* // Documentation and any `derive`'s.
80        $name:ident,        // Name of the corresponding `LazyLock`.
81        $dirs_fn:ident,   // Name of the `dirs` function to use, the PATH prefix.
82        $sub_dirs:literal // Any sub-directories to add onto the PATH.
83    ),* $(,)?) => {$(
84        // Create the `LazyLock` if needed, append
85        // the Cuprate directory string and return.
86        $(#[$attr])*
87        pub static $name: LazyLock<PathBuf> = LazyLock::new(|| {
88            // There's nothing we can do but panic if
89            // we cannot acquire critical system directories.
90            //
91            // Although, this realistically won't panic on
92            // normal systems for all OS's supported by `dirs`.
93            let mut path = dirs::$dirs_fn().unwrap();
94
95            // FIXME:
96            // Consider a user who does `HOME=/ ./cuprated`
97            //
98            // Should we say "that's stupid" and panic here?
99            // Or should it be respected?
100            // We really don't want a `rm -rf /` type of situation...
101            assert!(
102                path.parent().is_some(),
103                "SAFETY: returned OS PATH was either root or empty, aborting"
104            );
105
106            // Returned OS PATH should be absolute, not relative.
107            assert!(path.is_absolute(), "SAFETY: returned OS PATH was not absolute");
108
109            // Unconditionally prefix with the top-level Cuprate directory.
110            path.push(CUPRATE_DIR);
111
112            // Add any sub directories if specified in the macro.
113            if !$sub_dirs.is_empty() {
114                path.push($sub_dirs);
115            }
116
117            path
118        });
119    )*};
120}
121
122impl_path_lazylock! {
123    /// Cuprate's cache directory.
124    ///
125    /// This is the PATH used for any Cuprate cache files.
126    ///
127    /// | OS      | PATH                                    |
128    /// |---------|-----------------------------------------|
129    /// | Windows | `C:\Users\Alice\AppData\Local\Cuprate\` |
130    /// | macOS   | `/Users/Alice/Library/Caches/Cuprate/`  |
131    /// | Linux   | `/home/alice/.cache/cuprate/`           |
132    CUPRATE_CACHE_DIR,
133    cache_dir,
134    "",
135
136    /// Cuprate's config directory.
137    ///
138    /// This is the PATH used for any Cuprate configuration files.
139    ///
140    /// | OS      | PATH                                                |
141    /// |---------|-----------------------------------------------------|
142    /// | Windows | `C:\Users\Alice\AppData\Roaming\Cuprate\`           |
143    /// | macOS   | `/Users/Alice/Library/Application Support/Cuprate/` |
144    /// | Linux   | `/home/alice/.config/cuprate/`                      |
145    CUPRATE_CONFIG_DIR,
146    config_dir,
147    "",
148
149    /// Cuprate's data directory.
150    ///
151    /// This is the PATH used for any Cuprate data files.
152    ///
153    /// | OS      | PATH                                                |
154    /// |---------|-----------------------------------------------------|
155    /// | Windows | `C:\Users\Alice\AppData\Roaming\Cuprate\`           |
156    /// | macOS   | `/Users/Alice/Library/Application Support/Cuprate/` |
157    /// | Linux   | `/home/alice/.local/share/cuprate/`                 |
158    CUPRATE_DATA_DIR,
159    data_dir,
160    "",
161}
162
163/// Joins the [`Network`] to the [`Path`].
164///
165/// This will keep the path the same for [`Network::Mainnet`].
166fn path_with_network(path: &Path, network: Network) -> PathBuf {
167    match network {
168        Network::Mainnet => path.to_path_buf(),
169        network => path.join(network.to_string()),
170    }
171}
172
173/// Cuprate's blockchain directory.
174///
175/// This is the PATH used for any Cuprate blockchain files.
176///
177/// ```rust
178/// use cuprate_helper::{network::Network, fs::{CUPRATE_DATA_DIR, blockchain_path}};
179///
180/// assert_eq!(blockchain_path(&**CUPRATE_DATA_DIR, Network::Mainnet).as_path(), CUPRATE_DATA_DIR.join("blockchain"));
181/// assert_eq!(blockchain_path(&**CUPRATE_DATA_DIR, Network::Stagenet).as_path(), CUPRATE_DATA_DIR.join(Network::Stagenet.to_string()).join("blockchain"));
182/// assert_eq!(blockchain_path(&**CUPRATE_DATA_DIR, Network::Testnet).as_path(), CUPRATE_DATA_DIR.join(Network::Testnet.to_string()).join("blockchain"));
183/// ```
184pub fn blockchain_path(data_dir: &Path, network: Network) -> PathBuf {
185    path_with_network(data_dir, network).join("blockchain")
186}
187
188/// Cuprate's txpool directory.
189///
190/// This is the PATH used for any Cuprate txpool files.
191///
192/// ```rust
193/// use cuprate_helper::{network::Network, fs::{CUPRATE_DATA_DIR, txpool_path}};
194///
195/// assert_eq!(txpool_path(&**CUPRATE_DATA_DIR, Network::Mainnet).as_path(), CUPRATE_DATA_DIR.join("txpool"));
196/// assert_eq!(txpool_path(&**CUPRATE_DATA_DIR, Network::Stagenet).as_path(), CUPRATE_DATA_DIR.join(Network::Stagenet.to_string()).join("txpool"));
197/// assert_eq!(txpool_path(&**CUPRATE_DATA_DIR, Network::Testnet).as_path(), CUPRATE_DATA_DIR.join(Network::Testnet.to_string()).join("txpool"));
198/// ```
199pub fn txpool_path(data_dir: &Path, network: Network) -> PathBuf {
200    path_with_network(data_dir, network).join("txpool")
201}
202
203/// Cuprate's logs directory.
204///
205/// This is the PATH used for all Cuprate log files.
206///
207/// ```rust
208/// use cuprate_helper::{network::Network, fs::{CUPRATE_DATA_DIR, logs_path}};
209///
210/// assert_eq!(logs_path(&**CUPRATE_DATA_DIR, Network::Mainnet).as_path(), CUPRATE_DATA_DIR.join("logs"));
211/// assert_eq!(logs_path(&**CUPRATE_DATA_DIR, Network::Stagenet).as_path(), CUPRATE_DATA_DIR.join(Network::Stagenet.to_string()).join("logs"));
212/// assert_eq!(logs_path(&**CUPRATE_DATA_DIR, Network::Testnet).as_path(), CUPRATE_DATA_DIR.join(Network::Testnet.to_string()).join("logs"));
213/// ```
214pub fn logs_path(data_dir: &Path, network: Network) -> PathBuf {
215    path_with_network(data_dir, network).join("logs")
216}
217
218/// Cuprate's address-book directory.
219///
220/// This is the PATH used for any Cuprate address-book files.
221///
222/// ```rust
223/// use cuprate_helper::{network::Network, fs::{CUPRATE_CACHE_DIR, address_book_path}};
224///
225/// assert_eq!(address_book_path(&**CUPRATE_CACHE_DIR, Network::Mainnet).as_path(), CUPRATE_CACHE_DIR.join("addressbook"));
226/// assert_eq!(address_book_path(&**CUPRATE_CACHE_DIR, Network::Stagenet).as_path(), CUPRATE_CACHE_DIR.join(Network::Stagenet.to_string()).join("addressbook"));
227/// assert_eq!(address_book_path(&**CUPRATE_CACHE_DIR, Network::Testnet).as_path(), CUPRATE_CACHE_DIR.join(Network::Testnet.to_string()).join("addressbook"));
228/// ```
229pub fn address_book_path(cache_dir: &Path, network: Network) -> PathBuf {
230    path_with_network(cache_dir, network).join("addressbook")
231}
232
233//---------------------------------------------------------------------------------------------------- Tests
234#[cfg(test)]
235mod test {
236    use super::*;
237
238    // Sanity check every PATH defined in this file.
239    //
240    // Each new PATH should be added to this test:
241    // - It must be `is_absolute()`
242    // - It must `ends_with()` the expected end PATH for the OS
243    #[test]
244    fn path_sanity_check() {
245        // Array of (PATH, expected_path_as_string).
246        //
247        // The different OS's will set the expected path below.
248        let mut array = [
249            (&*CUPRATE_CACHE_DIR, ""),
250            (&*CUPRATE_CONFIG_DIR, ""),
251            (&*CUPRATE_DATA_DIR, ""),
252        ];
253
254        if cfg!(target_os = "windows") {
255            array[0].1 = r"AppData\Local\Cuprate";
256            array[1].1 = r"AppData\Roaming\Cuprate";
257            array[2].1 = r"AppData\Roaming\Cuprate";
258        } else if cfg!(target_os = "macos") {
259            array[0].1 = "Library/Caches/Cuprate";
260            array[1].1 = "Library/Application Support/Cuprate";
261            array[2].1 = "Library/Application Support/Cuprate";
262        } else {
263            // Assumes Linux.
264            array[0].1 = ".cache/cuprate";
265            array[1].1 = ".config/cuprate";
266            array[2].1 = ".local/share/cuprate";
267        };
268
269        for (path, expected) in array {
270            assert!(path.is_absolute());
271            assert!(path.ends_with(expected));
272        }
273    }
274}