Skip to content

Commit 41a2885

Browse files
authored
Replace glob with fs::read_dir to avoid path escaping issues. (#2528)
1 parent a74b3de commit 41a2885

2 files changed

Lines changed: 82 additions & 39 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ local.sh
1111
.stellar
1212
.zed
1313
node_modules/
14-
.DS_Store
14+
.DS_Store
15+
logs/
16+
ai-summary/

cmd/soroban-cli/src/commands/contract/alias/ls.rs

Lines changed: 79 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use clap::Parser;
22
use std::collections::HashMap;
3+
use std::ffi::OsStr;
34
use std::fmt::Debug;
45
use std::path::Path;
56
use std::{fs, process};
@@ -23,12 +24,6 @@ pub enum Error {
2324
#[error(transparent)]
2425
Network(#[from] network::Error),
2526

26-
#[error(transparent)]
27-
PatternError(#[from] glob::PatternError),
28-
29-
#[error(transparent)]
30-
GlobError(#[from] glob::GlobError),
31-
3227
#[error(transparent)]
3328
IoError(#[from] std::io::Error),
3429
}
@@ -57,57 +52,57 @@ impl Cmd {
5752
Ok(())
5853
}
5954

60-
fn read_from_config_dir(config_dir: &Path) -> Result<(), Error> {
61-
let pattern = config_dir
62-
.join("contract-ids")
63-
.join("*.json")
64-
.to_string_lossy()
65-
.into_owned();
66-
67-
let paths = glob::glob(&pattern)?;
68-
let mut found = false;
55+
fn collect_aliases(config_dir: &Path) -> Result<HashMap<String, Vec<AliasEntry>>, Error> {
56+
let contract_ids_dir = config_dir.join("contract-ids");
6957
let mut map: HashMap<String, Vec<AliasEntry>> = HashMap::new();
7058

71-
for path in paths {
72-
let path = path?;
59+
if !contract_ids_dir.is_dir() {
60+
return Ok(map);
61+
}
62+
63+
for entry in fs::read_dir(&contract_ids_dir)? {
64+
let path = entry?.path();
65+
66+
if path.extension() != Some(OsStr::new("json")) {
67+
continue;
68+
}
7369

7470
if let Some(alias) = path.file_stem() {
7571
let alias = alias.to_string_lossy().into_owned();
76-
let content = fs::read_to_string(path)?;
72+
let content = fs::read_to_string(&path)?;
7773
let data: alias::Data = serde_json::from_str(&content).unwrap_or_default();
7874

79-
for network_passphrase in data.ids.keys() {
80-
let network_passphrase = network_passphrase.clone();
81-
let contract = data
82-
.ids
83-
.get(&network_passphrase)
84-
.map(ToString::to_string)
85-
.unwrap_or_default();
75+
for (network_passphrase, contract_id) in &data.ids {
8676
let entry = AliasEntry {
8777
alias: alias.clone(),
88-
contract,
78+
contract: contract_id.clone(),
8979
};
9080

91-
let list = map.entry(network_passphrase.clone()).or_default();
92-
93-
list.push(entry.clone());
81+
map.entry(network_passphrase.clone())
82+
.or_default()
83+
.push(entry);
9484
}
9585
}
9686
}
9787

98-
for network_passphrase in map.keys() {
99-
if let Some(list) = map.clone().get_mut(network_passphrase) {
100-
println!("ℹ️ Aliases available for network '{network_passphrase}'");
88+
Ok(map)
89+
}
90+
91+
fn read_from_config_dir(config_dir: &Path) -> Result<(), Error> {
92+
let mut map = Self::collect_aliases(config_dir)?;
93+
let mut found = false;
10194

102-
list.sort_by(|a, b| a.alias.cmp(&b.alias));
95+
for (network_passphrase, list) in &mut map {
96+
println!("ℹ️ Aliases available for network '{network_passphrase}'");
10397

104-
for entry in list {
105-
found = true;
106-
println!("{}: {}", entry.alias, entry.contract);
107-
}
98+
list.sort_by(|a, b| a.alias.cmp(&b.alias));
10899

109-
println!();
100+
for entry in list.iter() {
101+
found = true;
102+
println!("{}: {}", entry.alias, entry.contract);
110103
}
104+
105+
println!();
111106
}
112107

113108
if !found {
@@ -119,3 +114,49 @@ impl Cmd {
119114
Ok(())
120115
}
121116
}
117+
118+
#[cfg(test)]
119+
mod tests {
120+
use super::*;
121+
use std::fs;
122+
123+
fn write_alias(dir: &Path, name: &str, network: &str, contract: &str) {
124+
let contract_ids_dir = dir.join("contract-ids");
125+
fs::create_dir_all(&contract_ids_dir).unwrap();
126+
let content = format!(r#"{{"ids":{{"{network}":"{contract}"}}}}"#);
127+
fs::write(contract_ids_dir.join(format!("{name}.json")), content).unwrap();
128+
}
129+
130+
#[test]
131+
fn glob_metacharacters_in_config_dir_are_treated_as_literal() {
132+
let tmp = tempfile::tempdir().unwrap();
133+
let base = tmp.path();
134+
135+
// Sibling directories that would match the glob `[12]` if unescaped.
136+
write_alias(&base.join("cfg1"), "alpha", "testnet", "CAAAA");
137+
write_alias(&base.join("cfg2"), "beta", "testnet", "CBBBB");
138+
139+
// The literal directory whose name contains bracket metacharacters.
140+
write_alias(&base.join("cfg[12]"), "gamma", "testnet", "CCCCC");
141+
142+
let map = Cmd::collect_aliases(&base.join("cfg[12]")).unwrap();
143+
144+
let aliases: Vec<&str> = map
145+
.values()
146+
.flat_map(|entries| entries.iter().map(|e| e.alias.as_str()))
147+
.collect();
148+
149+
assert!(
150+
aliases.contains(&"gamma"),
151+
"should read alias from the literal directory"
152+
);
153+
assert!(
154+
!aliases.contains(&"alpha"),
155+
"should not read from sibling cfg1"
156+
);
157+
assert!(
158+
!aliases.contains(&"beta"),
159+
"should not read from sibling cfg2"
160+
);
161+
}
162+
}

0 commit comments

Comments
 (0)