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}