Skip to main content

soroban_cli/commands/contract/alias/
ls.rs

1use clap::Parser;
2use std::collections::HashMap;
3use std::ffi::OsStr;
4use std::fmt::Debug;
5use std::fs;
6use std::path::Path;
7
8use crate::commands::config::network;
9use crate::config::locator::{print_deprecation_warning, Location};
10use crate::config::{alias, locator};
11
12#[derive(Parser, Debug, Clone)]
13#[group(skip)]
14pub struct Cmd {
15    #[command(flatten)]
16    pub config_locator: locator::Args,
17}
18
19#[derive(thiserror::Error, Debug)]
20pub enum Error {
21    #[error(transparent)]
22    Locator(#[from] locator::Error),
23
24    #[error(transparent)]
25    Network(#[from] network::Error),
26
27    #[error(transparent)]
28    IoError(#[from] std::io::Error),
29}
30
31#[derive(Debug, Clone)]
32struct AliasEntry {
33    alias: String,
34    contract: String,
35    builtin: bool,
36}
37
38impl Cmd {
39    pub fn run(&self) -> Result<(), Error> {
40        let config_dirs = self.config_locator.local_and_global()?;
41
42        for cfg in config_dirs {
43            match cfg {
44                Location::Local(config_dir) => {
45                    if config_dir.exists() {
46                        print_deprecation_warning(&config_dir);
47                    }
48                }
49                Location::Global(config_dir) => self.read_from_config_dir(&config_dir)?,
50            }
51        }
52
53        Ok(())
54    }
55
56    fn collect_aliases(config_dir: &Path) -> Result<HashMap<String, Vec<AliasEntry>>, Error> {
57        let contract_ids_dir = config_dir.join("contract-ids");
58        let mut map: HashMap<String, Vec<AliasEntry>> = HashMap::new();
59
60        if !contract_ids_dir.is_dir() {
61            return Ok(map);
62        }
63
64        for entry in fs::read_dir(&contract_ids_dir)? {
65            let path = entry?.path();
66
67            if path.extension() != Some(OsStr::new("json")) {
68                continue;
69            }
70
71            if let Some(alias) = path.file_stem() {
72                let alias = alias.to_string_lossy().into_owned();
73                let content = fs::read_to_string(&path)?;
74                let data: alias::Data = serde_json::from_str(&content).unwrap_or_default();
75
76                for (network_passphrase, contract_id) in &data.ids {
77                    let entry = AliasEntry {
78                        alias: alias.clone(),
79                        contract: contract_id.clone(),
80                        builtin: false,
81                    };
82
83                    map.entry(network_passphrase.clone())
84                        .or_default()
85                        .push(entry);
86                }
87            }
88        }
89
90        Ok(map)
91    }
92
93    fn read_from_config_dir(&self, config_dir: &Path) -> Result<(), Error> {
94        let mut map = Self::collect_aliases(config_dir)?;
95
96        // The built-in `native` alias resolves to the native asset contract for
97        // a network, so make sure every known network is listed even when it has
98        // no stored aliases of its own.
99        for (_, network, _) in self.config_locator.list_networks_long()? {
100            map.entry(network.network_passphrase).or_default();
101        }
102
103        for (network_passphrase, list) in &mut map {
104            if let Some(contract) =
105                alias::resolve_reserved(alias::NATIVE, &self.config_locator, network_passphrase)
106            {
107                list.push(AliasEntry {
108                    alias: alias::NATIVE.to_string(),
109                    contract: format!("{contract}"),
110                    builtin: true,
111                });
112            }
113
114            println!("ℹ️ Aliases available for network '{network_passphrase}'");
115
116            list.sort_by(|a, b| a.alias.cmp(&b.alias));
117
118            for entry in list.iter() {
119                let note = if entry.builtin {
120                    " (built-in)"
121                } else if alias::is_reserved(&entry.alias) {
122                    // A user alias whose name is now reserved is shadowed by the
123                    // built-in alias of the same name, so it no longer resolves.
124                    " (disabled)"
125                } else {
126                    ""
127                };
128                println!("{}: {}{note}", entry.alias, entry.contract);
129            }
130
131            println!();
132        }
133
134        Ok(())
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use std::fs;
142
143    fn write_alias(dir: &Path, name: &str, network: &str, contract: &str) {
144        let contract_ids_dir = dir.join("contract-ids");
145        fs::create_dir_all(&contract_ids_dir).unwrap();
146        let content = format!(r#"{{"ids":{{"{network}":"{contract}"}}}}"#);
147        fs::write(contract_ids_dir.join(format!("{name}.json")), content).unwrap();
148    }
149
150    #[test]
151    fn glob_metacharacters_in_config_dir_are_treated_as_literal() {
152        let tmp = tempfile::tempdir().unwrap();
153        let base = tmp.path();
154
155        // Sibling directories that would match the glob `[12]` if unescaped.
156        write_alias(&base.join("cfg1"), "alpha", "testnet", "CAAAA");
157        write_alias(&base.join("cfg2"), "beta", "testnet", "CBBBB");
158
159        // The literal directory whose name contains bracket metacharacters.
160        write_alias(&base.join("cfg[12]"), "gamma", "testnet", "CCCCC");
161
162        let map = Cmd::collect_aliases(&base.join("cfg[12]")).unwrap();
163
164        let aliases: Vec<&str> = map
165            .values()
166            .flat_map(|entries| entries.iter().map(|e| e.alias.as_str()))
167            .collect();
168
169        assert!(
170            aliases.contains(&"gamma"),
171            "should read alias from the literal directory"
172        );
173        assert!(
174            !aliases.contains(&"alpha"),
175            "should not read from sibling cfg1"
176        );
177        assert!(
178            !aliases.contains(&"beta"),
179            "should not read from sibling cfg2"
180        );
181    }
182}