1use directories::UserDirs;
2use itertools::Itertools;
3use serde::de::DeserializeOwned;
4use std::{
5 ffi::OsStr,
6 fmt::Display,
7 fs::{self, OpenOptions},
8 io::{self, Write},
9 path::{Path, PathBuf},
10 str::FromStr,
11};
12use stellar_strkey::{Contract, DecodeError};
13
14use crate::{
15 commands::{global, HEADING_GLOBAL},
16 print::Print,
17 signer::secure_store,
18 utils::find_config_dir,
19 xdr, Pwd,
20};
21
22use super::{
23 alias,
24 key::{self, Key},
25 network::{self, Network},
26 secret::Secret,
27 utils, Config,
28};
29
30#[derive(thiserror::Error, Debug)]
31pub enum Error {
32 #[error(transparent)]
33 TomlSerialize(#[from] toml::ser::Error),
34 #[error("Failed to find home directory")]
35 HomeDirNotFound,
36 #[error("Failed read current directory")]
37 CurrentDirNotFound,
38 #[error("Failed read current directory and no STELLAR_CONFIG_HOME is set")]
39 NoConfigEnvVar,
40 #[error("Failed to create directory: {path:?}")]
41 DirCreationFailed { path: PathBuf },
42 #[error("Failed to read secret's file: {path}.\nProbably need to use `stellar keys add`")]
43 SecretFileRead { path: PathBuf },
44 #[error("Failed to read network file: {path};\nProbably need to use `stellar network add`")]
45 NetworkFileRead { path: PathBuf },
46 #[error("Failed to read file: {path}")]
47 FileRead { path: PathBuf },
48 #[error(transparent)]
49 Toml(#[from] toml::de::Error),
50 #[error("Secret file failed to deserialize")]
51 Deserialization,
52 #[error("Failed to write identity file:{filepath}: {error}")]
53 IdCreationFailed { filepath: PathBuf, error: io::Error },
54 #[error("Secret file failed to deserialize")]
55 NetworkDeserialization,
56 #[error("Failed to write network file: {0}")]
57 NetworkCreationFailed(std::io::Error),
58 #[error("Error Identity directory is invalid: {name}")]
59 IdentityList { name: String },
60 #[error("Config file failed to serialize")]
63 ConfigSerialization,
64 #[error("STELLAR_CONFIG_HOME env variable is not a valid path. Got {0}")]
67 StellarConfigDir(String),
68 #[error("XDG_CONFIG_HOME env variable is not a valid path. Got {0}")]
69 XdgConfigHome(String),
70 #[error(transparent)]
71 Io(#[from] std::io::Error),
72 #[error("Failed to remove {0}: {1}")]
73 ConfigRemoval(String, String),
74 #[error("Failed to find config {0} for {1}")]
75 ConfigMissing(String, String),
76 #[error(transparent)]
77 String(#[from] std::string::FromUtf8Error),
78 #[error(transparent)]
79 Secret(#[from] crate::config::secret::Error),
80 #[error(transparent)]
81 Json(#[from] serde_json::Error),
82 #[error("cannot access config dir for alias file")]
83 CannotAccessConfigDir,
84 #[error("cannot access alias config file (no permission or doesn't exist)")]
85 CannotAccessAliasConfigFile,
86 #[error("cannot parse contract ID {0}: {1}")]
87 CannotParseContractId(String, DecodeError),
88 #[error("contract not found: {0}")]
89 ContractNotFound(String),
90 #[error("Failed to read upgrade check file: {path}: {error}")]
91 UpgradeCheckReadFailed { path: PathBuf, error: io::Error },
92 #[error("Failed to write upgrade check file: {path}: {error}")]
93 UpgradeCheckWriteFailed { path: PathBuf, error: io::Error },
94 #[error("Contract alias {0}, cannot overlap with key")]
95 ContractAliasCannotOverlapWithKey(String),
96 #[error("Key cannot {0} cannot overlap with contract alias")]
97 KeyCannotOverlapWithContractAlias(String),
98 #[error(transparent)]
99 SecureStore(#[from] secure_store::Error),
100 #[error("Only private keys and seed phrases are supported for getting private keys {0}")]
101 SecretKeyOnly(String),
102 #[error(transparent)]
103 Key(#[from] key::Error),
104 #[error("Unable to get project directory")]
105 ProjectDirsError(),
106 #[error(transparent)]
107 InvalidName(#[from] utils::Error),
108}
109
110#[derive(Debug, clap::Args, Default, Clone)]
111#[group(skip)]
112pub struct Args {
113 #[arg(long, global = true, help_heading = HEADING_GLOBAL)]
116 pub config_dir: Option<PathBuf>,
117}
118
119#[derive(Clone)]
120pub enum Location {
121 Local(PathBuf),
122 Global(PathBuf),
123}
124
125impl Display for Location {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 write!(
128 f,
129 "{} {:?}",
130 match self {
131 Location::Local(_) => "Local",
132 Location::Global(_) => "Global",
133 },
134 self.as_ref()
135 )
136 }
137}
138
139impl AsRef<Path> for Location {
140 fn as_ref(&self) -> &Path {
141 match self {
142 Location::Local(p) | Location::Global(p) => p.as_path(),
143 }
144 }
145}
146
147impl Location {
148 #[must_use]
149 pub fn wrap(&self, p: PathBuf) -> Self {
150 match self {
151 Location::Local(_) => Location::Local(p),
152 Location::Global(_) => Location::Global(p),
153 }
154 }
155}
156
157impl Args {
158 pub fn config_dir(&self) -> Result<PathBuf, Error> {
159 self.global_config_path()
160 }
161
162 pub fn local_and_global(&self) -> Result<[Location; 2], Error> {
163 Ok([
164 Location::Local(self.local_config()?),
165 Location::Global(self.global_config_path()?),
166 ])
167 }
168
169 pub fn local_config(&self) -> Result<PathBuf, Error> {
170 let pwd = self.current_dir()?;
171 Ok(find_config_dir(pwd.clone()).unwrap_or_else(|_| pwd.join(".stellar")))
172 }
173
174 pub fn current_dir(&self) -> Result<PathBuf, Error> {
175 self.config_dir.as_ref().map_or_else(
176 || std::env::current_dir().map_err(|_| Error::CurrentDirNotFound),
177 |pwd| Ok(pwd.clone()),
178 )
179 }
180
181 pub fn write_identity(&self, name: &str, secret: &Secret) -> Result<PathBuf, Error> {
182 if let Ok(Some(_)) = self.load_contract_from_alias(name) {
183 return Err(Error::KeyCannotOverlapWithContractAlias(name.to_owned()));
184 }
185 KeyType::Identity.write(name, secret, &self.config_dir()?)
186 }
187
188 pub fn write_public_key(
189 &self,
190 name: &str,
191 public_key: &stellar_strkey::ed25519::PublicKey,
192 ) -> Result<PathBuf, Error> {
193 self.write_key(name, &public_key.into())
194 }
195
196 pub fn write_key(&self, name: &str, key: &Key) -> Result<PathBuf, Error> {
197 KeyType::Identity.write(name, key, &self.config_dir()?)
198 }
199
200 pub fn write_network(&self, name: &str, network: &Network) -> Result<PathBuf, Error> {
201 KeyType::Network.write(name, network, &self.config_dir()?)
202 }
203
204 pub fn write_default_network(&self, name: &str) -> Result<(), Error> {
205 Config::new()?.set_network(name).save()
206 }
207
208 pub fn write_default_identity(&self, name: &str) -> Result<(), Error> {
209 Config::new()?.set_identity(name).save()
210 }
211
212 pub fn write_default_inclusion_fee(&self, inclusion_fee: u32) -> Result<(), Error> {
213 Config::new()?.set_inclusion_fee(inclusion_fee).save()
214 }
215
216 pub fn unset_default_identity(&self) -> Result<(), Error> {
217 Config::new()?.unset_identity().save()
218 }
219
220 pub fn unset_default_network(&self) -> Result<(), Error> {
221 Config::new()?.unset_network().save()
222 }
223
224 pub fn unset_default_inclusion_fee(&self) -> Result<(), Error> {
225 Config::new()?.unset_inclusion_fee().save()
226 }
227
228 pub fn list_identities(&self) -> Result<Vec<String>, Error> {
229 Ok(KeyType::Identity
230 .list_paths(&self.local_and_global()?)?
231 .into_iter()
232 .map(|(name, _)| name)
233 .collect())
234 }
235
236 pub fn list_identities_long(&self) -> Result<Vec<(String, String)>, Error> {
237 Ok(KeyType::Identity
238 .list_paths(&self.local_and_global()?)
239 .into_iter()
240 .flatten()
241 .map(|(name, location)| {
242 let path = match location {
243 Location::Local(path) | Location::Global(path) => path,
244 };
245 (name, format!("{}", path.display()))
246 })
247 .collect())
248 }
249
250 pub fn list_networks(&self) -> Result<Vec<String>, Error> {
251 let saved_networks = KeyType::Network
252 .list_paths(&self.local_and_global()?)
253 .into_iter()
254 .flatten()
255 .map(|x| x.0);
256 let default_networks = network::DEFAULTS.keys().map(ToString::to_string);
257 Ok(saved_networks.chain(default_networks).unique().collect())
258 }
259
260 pub fn list_networks_long(&self) -> Result<Vec<(String, Network, String)>, Error> {
261 let saved_networks = KeyType::Network
262 .list_paths(&self.local_and_global()?)
263 .into_iter()
264 .flatten()
265 .filter_map(|(name, location)| {
266 Some((
267 name,
268 KeyType::read_from_path::<Network>(location.as_ref()).ok()?,
269 location.to_string(),
270 ))
271 });
272 let default_networks = network::DEFAULTS
273 .into_iter()
274 .map(|(name, network)| ((*name).to_string(), network.into(), "Default".to_owned()));
275 Ok(saved_networks.chain(default_networks).collect())
276 }
277
278 pub fn read_identity(&self, name: &str) -> Result<Key, Error> {
279 utils::validate_name(name)?;
280 KeyType::Identity.read_with_global(name, self)
281 }
282
283 pub fn read_identity_with_location(&self, name: &str) -> Result<(Key, Location), Error> {
285 utils::validate_name(name)?;
286 KeyType::Identity.read_with_global_with_location(name, self)
287 }
288
289 pub fn read_key(&self, key_or_name: &str) -> Result<Key, Error> {
290 key_or_name
291 .parse()
292 .or_else(|_| self.read_identity(key_or_name))
293 }
294
295 pub fn get_secret_key(&self, key_or_name: &str) -> Result<Secret, Error> {
296 match self.read_key(key_or_name)? {
297 Key::Secret(s) => Ok(s),
298 _ => Err(Error::SecretKeyOnly(key_or_name.to_string())),
299 }
300 }
301
302 pub fn get_public_key(
303 &self,
304 key_or_name: &str,
305 hd_path: Option<usize>,
306 ) -> Result<xdr::MuxedAccount, Error> {
307 Ok(self.read_key(key_or_name)?.muxed_account(hd_path)?)
308 }
309
310 pub fn read_network(&self, name: &str) -> Result<Network, Error> {
311 utils::validate_name(name)?;
312 let res = KeyType::Network.read_with_global(name, self);
313 if let Err(Error::ConfigMissing(_, _)) = &res {
314 let Some(network) = network::DEFAULTS.get(name) else {
315 return res;
316 };
317 return Ok(network.into());
318 }
319 res
320 }
321
322 pub fn remove_identity(&self, name: &str, global_args: &global::Args) -> Result<(), Error> {
323 let print = Print::new(global_args.quiet);
324 let identity = self.read_identity(name)?;
325
326 if let Key::Secret(Secret::SecureStore { entry_name }) = identity {
327 secure_store::delete_secret(&print, &entry_name)?;
328 }
329
330 print.infoln("Removing the key's cli config file");
331 KeyType::Identity.remove(name, &self.config_dir()?)
332 }
333
334 pub fn remove_network(&self, name: &str) -> Result<(), Error> {
335 KeyType::Network.remove(name, &self.config_dir()?)
336 }
337
338 fn load_contract_from_alias(&self, alias: &str) -> Result<Option<alias::Data>, Error> {
339 utils::validate_name(alias)?;
340 let file_name = format!("{alias}.json");
341 let config_dirs = self.local_and_global()?;
342 let local = &config_dirs[0];
343 let global = &config_dirs[1];
344
345 match local {
346 Location::Local(config_dir) => {
347 let path = config_dir.join("contract-ids").join(&file_name);
348 if path.exists() {
349 print_deprecation_warning(config_dir);
350
351 let content = fs::read_to_string(path)?;
352 let data: alias::Data = serde_json::from_str(&content).unwrap_or_default();
353
354 return Ok(Some(data));
355 }
356 }
357 Location::Global(_) => unreachable!(),
358 }
359
360 match global {
361 Location::Global(config_dir) => {
362 let path = config_dir.join("contract-ids").join(&file_name);
363 if !path.exists() {
364 return Ok(None);
365 }
366
367 let content = fs::read_to_string(path)?;
368 let data: alias::Data = serde_json::from_str(&content).unwrap_or_default();
369
370 Ok(Some(data))
371 }
372 Location::Local(_) => unreachable!(),
373 }
374 }
375
376 fn alias_path(&self, alias: &str) -> Result<PathBuf, Error> {
377 utils::validate_name(alias)?;
378 let file_name = format!("{alias}.json");
379 let config_dir = self.config_dir()?;
380 Ok(config_dir.join("contract-ids").join(file_name))
381 }
382
383 pub fn save_contract_id(
384 &self,
385 network_passphrase: &str,
386 contract_id: &stellar_strkey::Contract,
387 alias: &str,
388 ) -> Result<(), Error> {
389 if self.read_identity(alias).is_ok() {
390 return Err(Error::ContractAliasCannotOverlapWithKey(alias.to_owned()));
391 }
392 let path = self.alias_path(alias)?;
393 let dir = path.parent().ok_or(Error::CannotAccessConfigDir)?;
394
395 #[cfg(unix)]
396 {
397 use std::os::unix::fs::DirBuilderExt;
398 std::fs::DirBuilder::new()
399 .recursive(true)
400 .mode(0o700)
401 .create(dir)
402 .map_err(|_| Error::CannotAccessConfigDir)?;
403 }
404
405 #[cfg(not(unix))]
406 std::fs::create_dir_all(dir).map_err(|_| Error::CannotAccessConfigDir)?;
407
408 let content = fs::read_to_string(&path).unwrap_or_default();
409 let mut data: alias::Data = serde_json::from_str(&content).unwrap_or_default();
410
411 data.ids
412 .insert(network_passphrase.into(), contract_id.to_string());
413
414 let content = serde_json::to_string(&data)?;
415
416 #[cfg(unix)]
417 {
418 use std::io::Write as _;
419 use std::os::unix::fs::OpenOptionsExt;
420 let mut to_file = OpenOptions::new()
421 .create(true)
422 .truncate(true)
423 .write(true)
424 .mode(0o600)
425 .open(&path)?;
426 to_file.write_all(content.as_bytes())?;
427 fix_config_permissions();
428 }
429
430 #[cfg(not(unix))]
431 {
432 let mut to_file = OpenOptions::new()
433 .create(true)
434 .truncate(true)
435 .write(true)
436 .open(path)?;
437 to_file.write_all(content.as_bytes())?;
438 }
439
440 Ok(())
441 }
442
443 pub fn remove_contract_id(&self, network_passphrase: &str, alias: &str) -> Result<(), Error> {
444 let path = self.alias_path(alias)?;
445
446 if !path.is_file() {
447 return Err(Error::CannotAccessAliasConfigFile);
448 }
449
450 let content = fs::read_to_string(&path).unwrap_or_default();
451 let mut data: alias::Data = serde_json::from_str(&content).unwrap_or_default();
452
453 let mut to_file = OpenOptions::new()
454 .create(true)
455 .truncate(true)
456 .write(true)
457 .open(path)?;
458
459 data.ids.remove::<str>(network_passphrase);
460
461 let content = serde_json::to_string(&data)?;
462
463 Ok(to_file.write_all(content.as_bytes())?)
464 }
465
466 pub fn get_contract_id(
467 &self,
468 alias: &str,
469 network_passphrase: &str,
470 ) -> Result<Option<Contract>, Error> {
471 let Some(alias_data) = self.load_contract_from_alias(alias)? else {
472 return Ok(None);
473 };
474
475 alias_data
476 .ids
477 .get(network_passphrase)
478 .map(|id| id.parse())
479 .transpose()
480 .map_err(|e| Error::CannotParseContractId(alias.to_owned(), e))
481 }
482
483 pub fn resolve_contract_id(
484 &self,
485 alias_or_contract_id: &str,
486 network_passphrase: &str,
487 ) -> Result<Contract, Error> {
488 let Some(contract) = self.get_contract_id(alias_or_contract_id, network_passphrase)? else {
489 return alias_or_contract_id
490 .parse()
491 .map_err(|e| Error::CannotParseContractId(alias_or_contract_id.to_owned(), e));
492 };
493 Ok(contract)
494 }
495
496 pub fn global_config_path(&self) -> Result<PathBuf, Error> {
497 if let Some(config_dir) = &self.config_dir {
498 return Ok(config_dir.clone());
499 }
500
501 global_config_path()
502 }
503}
504
505pub fn print_deprecation_warning(dir: &Path) {
506 let print = Print::new(false);
507 let global_dir = global_config_path().expect("Couldn't retrieve global directory.");
508 let global_dir = fs::canonicalize(&global_dir).expect("Couldn't expand global directory.");
509
510 if dir == global_dir {
512 return;
513 }
514
515 print.warnln(format!("A local config was found at {dir:?}."));
516 print.blankln(" Local config is deprecated and will be removed in the future.".to_string());
517 print.blankln(format!(
518 " Run `stellar config migrate` to move the local config into the global config ({global_dir:?})."
519 ));
520}
521
522impl Pwd for Args {
523 fn set_pwd(&mut self, pwd: &Path) {
524 self.config_dir = Some(pwd.to_path_buf());
525 }
526}
527
528#[cfg(unix)]
529fn fix_config_permissions() {
530 use std::os::unix::fs::PermissionsExt;
531
532 let Ok(root) = global_config_path() else {
533 return;
534 };
535
536 let mut bad_dirs = Vec::new();
537 let mut bad_files = Vec::new();
538 let mut stack = vec![root];
539
540 while let Some(dir) = stack.pop() {
541 if let Ok(meta) = std::fs::metadata(&dir) {
542 if meta.permissions().mode() & 0o777 != 0o700 {
543 bad_dirs.push(dir.clone());
544 }
545 }
546
547 if let Ok(entries) = std::fs::read_dir(&dir) {
548 for entry in entries.filter_map(Result::ok) {
549 let path = entry.path();
550
551 if path.is_dir() {
552 stack.push(path);
553 } else if let Ok(meta) = std::fs::metadata(&path) {
554 if meta.permissions().mode() & 0o777 != 0o600 {
555 bad_files.push(path);
556 }
557 }
558 }
559 }
560 }
561
562 let print = Print::new(false);
563
564 if !bad_dirs.is_empty() {
565 print.warnln("Updated config directories permissions to 0700.");
566
567 for dir in bad_dirs {
568 let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700));
569 }
570 }
571
572 if !bad_files.is_empty() {
573 print.warnln("Updated config files permissions to 0600.");
574
575 for file in bad_files {
576 let _ = std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o600));
577 }
578 }
579}
580
581pub fn ensure_directory(dir: PathBuf) -> Result<PathBuf, Error> {
582 let parent = dir.parent().ok_or(Error::HomeDirNotFound)?;
583
584 #[cfg(unix)]
585 {
586 use std::os::unix::fs::DirBuilderExt;
587 std::fs::DirBuilder::new()
588 .recursive(true)
589 .mode(0o700)
590 .create(parent)
591 .map_err(|_| dir_creation_failed(parent))?;
592 fix_config_permissions();
593 }
594
595 #[cfg(not(unix))]
596 std::fs::create_dir_all(parent).map_err(|_| dir_creation_failed(parent))?;
597
598 Ok(dir)
599}
600
601fn dir_creation_failed(p: &Path) -> Error {
602 Error::DirCreationFailed {
603 path: p.to_path_buf(),
604 }
605}
606
607pub enum KeyType {
608 Identity,
609 Network,
610 ContractIds,
611}
612
613impl Display for KeyType {
614 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
615 write!(
616 f,
617 "{}",
618 match self {
619 KeyType::Identity => "identity",
620 KeyType::Network => "network",
621 KeyType::ContractIds => "contract-ids",
622 }
623 )
624 }
625}
626
627impl KeyType {
628 pub fn read_from_path<T: DeserializeOwned>(path: &Path) -> Result<T, Error> {
629 let data = fs::read_to_string(path).map_err(|_| Error::NetworkFileRead {
630 path: path.to_path_buf(),
631 })?;
632
633 #[cfg(unix)]
634 fix_config_permissions();
635
636 Ok(toml::from_str(&data)?)
637 }
638
639 pub fn read_with_global<T: DeserializeOwned>(
640 &self,
641 key: &str,
642 locator: &Args,
643 ) -> Result<T, Error> {
644 Ok(self.read_with_global_with_location(key, locator)?.0)
645 }
646
647 pub fn read_with_global_with_location<T: DeserializeOwned>(
648 &self,
649 key: &str,
650 locator: &Args,
651 ) -> Result<(T, Location), Error> {
652 for location in locator.local_and_global()? {
653 let path = self.path(location.as_ref(), key);
654
655 if let Ok(t) = Self::read_from_path(&path) {
656 if let Location::Local(config_dir) = location.clone() {
657 print_deprecation_warning(&config_dir);
658 }
659
660 return Ok((t, location));
661 }
662 }
663 Err(Error::ConfigMissing(self.to_string(), key.to_string()))
664 }
665
666 pub fn write<T: serde::Serialize>(
667 &self,
668 key: &str,
669 value: &T,
670 pwd: &Path,
671 ) -> Result<PathBuf, Error> {
672 let filepath = ensure_directory(self.path(pwd, key))?;
673 let data = toml::to_string(value).map_err(|_| Error::ConfigSerialization)?;
674 #[cfg(unix)]
675 {
676 use std::io::Write as _;
677 use std::os::unix::fs::OpenOptionsExt;
678 let mut file = std::fs::OpenOptions::new()
679 .write(true)
680 .create(true)
681 .truncate(true)
682 .mode(0o600)
683 .open(&filepath)
684 .map_err(|error| Error::IdCreationFailed {
685 filepath: filepath.clone(),
686 error,
687 })?;
688 file.write_all(data.as_bytes())
689 .map_err(|error| Error::IdCreationFailed {
690 filepath: filepath.clone(),
691 error,
692 })?;
693 }
694
695 #[cfg(not(unix))]
696 std::fs::write(&filepath, data).map_err(|error| Error::IdCreationFailed {
697 filepath: filepath.clone(),
698 error,
699 })?;
700
701 #[cfg(unix)]
702 fix_config_permissions();
703
704 Ok(filepath)
705 }
706
707 fn root(&self, pwd: &Path) -> PathBuf {
708 pwd.join(self.to_string())
709 }
710
711 pub fn path(&self, pwd: &Path, key: &str) -> PathBuf {
712 let mut path = self.root(pwd).join(key);
713 match self {
714 KeyType::Identity | KeyType::Network => path.set_extension("toml"),
715 KeyType::ContractIds => path.set_extension("json"),
716 };
717 path
718 }
719
720 pub fn list_paths(&self, paths: &[Location]) -> Result<Vec<(String, Location)>, Error> {
721 Ok(paths
722 .iter()
723 .unique_by(|p| location_to_string(p))
724 .flat_map(|p| self.list(p, true).unwrap_or_default())
725 .collect())
726 }
727
728 pub fn list_paths_silent(&self, paths: &[Location]) -> Result<Vec<(String, Location)>, Error> {
729 Ok(paths
730 .iter()
731 .flat_map(|p| self.list(p, false).unwrap_or_default())
732 .collect())
733 }
734
735 #[allow(unused_variables)]
736 pub fn list(
737 &self,
738 pwd: &Location,
739 print_warning: bool,
740 ) -> Result<Vec<(String, Location)>, Error> {
741 let path = self.root(pwd.as_ref());
742 if path.exists() {
743 let mut files = self.read_dir(&path)?;
744 files.sort();
745
746 if let Location::Local(config_dir) = pwd {
747 if files.len() > 1 && print_warning {
748 print_deprecation_warning(config_dir);
749 }
750 }
751
752 Ok(files
753 .into_iter()
754 .map(|(name, p)| (name, pwd.wrap(p)))
755 .collect())
756 } else {
757 Ok(vec![])
758 }
759 }
760
761 fn read_dir(&self, dir: &Path) -> Result<Vec<(String, PathBuf)>, Error> {
762 let contents = std::fs::read_dir(dir)?;
763 let mut res = vec![];
764 for entry in contents.filter_map(Result::ok) {
765 let path = entry.path();
766 let extension = match self {
767 KeyType::Identity | KeyType::Network => "toml",
768 KeyType::ContractIds => "json",
769 };
770 if let Some(ext) = path.extension().and_then(OsStr::to_str) {
771 if ext == extension {
772 if let Some(os_str) = path.file_stem() {
773 res.push((os_str.to_string_lossy().trim().to_string(), path));
774 }
775 }
776 }
777 }
778 res.sort();
779 Ok(res)
780 }
781
782 pub fn remove(&self, key: &str, pwd: &Path) -> Result<(), Error> {
783 let path = self.path(pwd, key);
784
785 if path.exists() {
786 std::fs::remove_file(&path)
787 .map_err(|_| Error::ConfigRemoval(self.to_string(), key.to_string()))
788 } else {
789 Ok(())
790 }
791 }
792}
793
794fn global_config_path() -> Result<PathBuf, Error> {
795 if let Ok(config_home) = std::env::var("STELLAR_CONFIG_HOME") {
796 return PathBuf::from_str(&config_home).map_err(|_| Error::StellarConfigDir(config_home));
797 }
798
799 let config_dir = if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME") {
800 PathBuf::from_str(&config_home).map_err(|_| Error::XdgConfigHome(config_home))?
801 } else {
802 UserDirs::new()
803 .ok_or(Error::HomeDirNotFound)?
804 .home_dir()
805 .join(".config")
806 };
807
808 let soroban_dir = config_dir.join("soroban");
809 let stellar_dir = config_dir.join("stellar");
810 let soroban_exists = soroban_dir.exists();
811 let stellar_exists = stellar_dir.exists();
812
813 if stellar_exists && soroban_exists {
814 tracing::warn!("the .stellar and .soroban config directories exist at path {config_dir:?}, using the .stellar");
815 }
816
817 if stellar_exists {
818 return Ok(stellar_dir);
819 }
820
821 if soroban_exists {
822 return Ok(soroban_dir);
823 }
824
825 Ok(stellar_dir)
826}
827
828fn location_to_string(location: &Location) -> String {
829 match location {
830 Location::Local(p) | Location::Global(p) => fs::canonicalize(AsRef::<Path>::as_ref(p))
831 .unwrap_or(p.clone())
832 .display()
833 .to_string(),
834 }
835}
836
837pub fn cli_config_file() -> Result<PathBuf, Error> {
840 Ok(global_config_path()?.join("config.toml"))
841}
842
843#[cfg(all(test, unix))]
844mod tests {
845 use super::*;
846 use std::collections::HashMap;
847
848 #[test]
849 fn test_write_sets_file_permissions_to_0600() {
850 use std::os::unix::fs::PermissionsExt;
851
852 let dir = tempfile::tempdir().unwrap();
853 let value: HashMap<String, String> = HashMap::new();
854 let path = KeyType::Identity
855 .write("test-key", &value, dir.path())
856 .unwrap();
857
858 let perms = std::fs::metadata(&path).unwrap().permissions();
859
860 assert_eq!(
861 perms.mode() & 0o777,
862 0o600,
863 "identity file should be owner-only readable (0600), got {:o}",
864 perms.mode() & 0o777
865 );
866 }
867
868 #[test]
869 fn test_ensure_directory_sets_dir_permissions_to_0700() {
870 use std::os::unix::fs::PermissionsExt;
871
872 let dir = tempfile::tempdir().unwrap();
873 let target = dir.path().join("sub").join("file.toml");
874 ensure_directory(target).unwrap();
875
876 let perms = std::fs::metadata(dir.path().join("sub"))
877 .unwrap()
878 .permissions();
879
880 assert_eq!(
881 perms.mode() & 0o777,
882 0o700,
883 "identity directory should be owner-only (0700), got {:o}",
884 perms.mode() & 0o777
885 );
886 }
887}