1use std::fmt::{Display, Formatter, Result as FMTResult};
2
3use crate::error::ContractError;
4use crate::os::vfs::{vfs_resolve_symlink, PATH_REGEX, PROTOCOL_PATH_REGEX};
5use crate::{ado_contract::ADOContract, os::vfs::vfs_resolve_path};
6use cosmwasm_schema::cw_serde;
7use cosmwasm_std::{Addr, Api, Deps, QuerierWrapper, Storage};
8use lazy_static::lazy_static;
9
10lazy_static! {
11 static ref ANDR_ADDR_REGEX: String = format!(
12 "({re1})|({re2})|({re3})|({re4})",
14 re1 = PROTOCOL_PATH_REGEX,
16 re2 = PATH_REGEX,
18 re3 = r"^[a-z0-9]{2,}$",
20 re4 = r"^\.(/[A-Za-z0-9.\-_]{2,40}?)*(/)?$",
22 );
23}
24
25#[cw_serde]
36pub struct AndrAddr(#[schemars(regex = "ANDR_ADDR_REGEX")] String);
37
38impl AndrAddr {
39 #[inline]
40 pub fn as_str(&self) -> &str {
41 self.0.as_str()
42 }
43
44 #[inline]
45 pub fn as_bytes(&self) -> &[u8] {
46 self.0.as_bytes()
47 }
48
49 #[inline]
50 pub fn into_string(self) -> String {
51 self.0
52 }
53
54 #[inline]
55 pub fn from_string(addr: impl Into<String>) -> AndrAddr {
56 AndrAddr(addr.into())
57 }
58
59 #[inline]
60 pub fn to_lowercase(&self) -> AndrAddr {
61 AndrAddr(self.0.to_lowercase())
62 }
63
64 pub fn validate(&self, api: &dyn Api) -> Result<(), ContractError> {
70 match self.is_vfs_path() || self.is_addr(api) {
71 true => Ok(()),
72 false => Err(ContractError::InvalidAddress {}),
73 }
74 }
75
76 pub fn get_raw_address(&self, deps: &Deps) -> Result<Addr, ContractError> {
82 if !self.is_vfs_path() {
83 return Ok(deps.api.addr_validate(&self.0)?);
84 }
85
86 let contract = ADOContract::default();
87 let vfs_contract = contract.get_vfs_address(deps.storage, &deps.querier)?;
88 self.get_raw_address_from_vfs(deps, vfs_contract)
89 }
90
91 pub fn get_raw_address_from_vfs(
97 &self,
98 deps: &Deps,
99 vfs_contract: impl Into<String>,
100 ) -> Result<Addr, ContractError> {
101 match self.is_vfs_path() {
102 false => Ok(deps.api.addr_validate(&self.0)?),
103 true => {
104 let vfs_contract: String = vfs_contract.into();
105 let valid_vfs_path =
107 self.local_path_to_vfs_path(deps.storage, &deps.querier, vfs_contract.clone())?;
108 let vfs_addr = Addr::unchecked(vfs_contract);
109 vfs_resolve_path(valid_vfs_path.clone(), vfs_addr, &deps.querier)
110 .ok()
111 .ok_or(ContractError::InvalidPathname {
112 error: Some(format!(
113 "{:?} does not exist in the file system",
114 valid_vfs_path.0
115 )),
116 })
117 }
118 }
119 }
120
121 pub fn local_path_to_vfs_path(
123 &self,
124 storage: &dyn Storage,
125 querier: &QuerierWrapper,
126 vfs_contract: impl Into<String>,
127 ) -> Result<AndrAddr, ContractError> {
128 match self.is_local_path() {
129 true => {
130 let app_contract = ADOContract::default().get_app_contract(storage)?;
131 match app_contract {
132 None => Err(ContractError::AppContractNotSpecified {}),
133 Some(app_contract) => {
134 let replaced = AndrAddr(self.0.replace("./", &format!("~{app_contract}/")));
135 vfs_resolve_symlink(replaced, vfs_contract, querier)
136 }
137 }
138 }
139 false => Ok(self.clone()),
140 }
141 }
142
143 pub fn is_local_path(&self) -> bool {
145 self.0.starts_with("./")
146 }
147
148 pub fn is_vfs_path(&self) -> bool {
150 self.is_local_path()
151 || self.0.starts_with('/')
152 || self.0.split("://").count() > 1
153 || self.0.split('/').count() > 1
154 || self.0.starts_with('~')
155 }
156
157 pub fn is_addr(&self, api: &dyn Api) -> bool {
159 api.addr_validate(&self.0).is_ok()
160 }
161
162 pub fn get_chain(&self) -> Option<&str> {
168 match self.get_protocol() {
169 None => None,
170 Some(..) => {
171 let start = self.0.find("://").unwrap() + 3;
172 let end = self.0[start..]
173 .find('/')
174 .unwrap_or_else(|| self.0[start..].len());
175 Some(&self.0[start..start + end])
176 }
177 }
178 }
179
180 pub fn get_protocol(&self) -> Option<&str> {
186 if !self.is_vfs_path() {
187 None
188 } else {
189 let mut split = self.0.split("://");
190 if split.clone().count() == 1 {
191 None
192 } else {
193 Some(split.next().unwrap())
194 }
195 }
196 }
197
198 pub fn get_raw_path(&self) -> &str {
204 if !self.is_vfs_path() {
205 self.0.as_str()
206 } else {
207 match self.get_protocol() {
208 None => self.0.as_str(),
209 Some(..) => {
210 let start = self.0.find("://").unwrap() + 3;
211 let end = self.0[start..]
212 .find('/')
213 .unwrap_or_else(|| self.0[start..].len());
214 &self.0[start + end..]
215 }
216 }
217 }
218 }
219
220 pub fn get_root_dir(&self) -> &str {
226 match self.is_vfs_path() {
227 false => self.0.as_str(),
228 true => match self.is_local_path() {
229 true => self.0.as_str(),
230 false => {
231 let raw_path = self.get_raw_path();
232 if raw_path.starts_with('~') {
233 return "home";
234 }
235 raw_path.split('/').nth(1).unwrap()
236 }
237 },
238 }
239 }
240}
241
242impl Display for AndrAddr {
243 fn fmt(&self, f: &mut Formatter) -> FMTResult {
244 write!(f, "{}", &self.0)
245 }
246}
247
248impl AsRef<str> for AndrAddr {
249 #[inline]
250 fn as_ref(&self) -> &str {
251 self.as_str()
252 }
253}
254
255impl PartialEq<&str> for AndrAddr {
256 fn eq(&self, rhs: &&str) -> bool {
257 self.0 == *rhs
258 }
259}
260
261impl PartialEq<AndrAddr> for &str {
262 fn eq(&self, rhs: &AndrAddr) -> bool {
263 *self == rhs.0
264 }
265}
266
267impl PartialEq<String> for AndrAddr {
268 fn eq(&self, rhs: &String) -> bool {
269 &self.0 == rhs
270 }
271}
272
273impl PartialEq<AndrAddr> for String {
274 fn eq(&self, rhs: &AndrAddr) -> bool {
275 self == &rhs.0
276 }
277}
278
279impl From<AndrAddr> for String {
280 fn from(addr: AndrAddr) -> Self {
281 addr.0
282 }
283}
284
285impl From<&AndrAddr> for String {
286 fn from(addr: &AndrAddr) -> Self {
287 addr.0.clone()
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use cosmwasm_std::testing::mock_dependencies;
294 use regex::Regex;
295
296 use super::*;
297 struct ValidateRegexTestCase {
298 name: &'static str,
299 input: &'static str,
300 should_err: bool,
301 }
302
303 #[test]
304 fn test_validate() {
305 let deps = mock_dependencies();
306 let addr = AndrAddr("cosmos1...".to_string());
307 assert!(addr.validate(&deps.api).is_ok());
308
309 let addr = AndrAddr("ibc://cosmoshub-4/home/user/app/component".to_string());
310 assert!(addr.validate(&deps.api).is_ok());
311
312 let addr = AndrAddr("/home/user/app/component".to_string());
313 assert!(addr.validate(&deps.api).is_ok());
314
315 let addr = AndrAddr("./user/app/component".to_string());
316 assert!(addr.validate(&deps.api).is_ok());
317
318 let addr = AndrAddr("1".to_string());
319 assert!(addr.validate(&deps.api).is_err());
320 }
321
322 #[test]
323 fn test_is_vfs() {
324 let addr = AndrAddr("/home/user/app/component".to_string());
325 assert!(addr.is_vfs_path());
326
327 let addr = AndrAddr("./user/app/component".to_string());
328 assert!(addr.is_vfs_path());
329
330 let addr = AndrAddr("ibc://chain/home/user/app/component".to_string());
331 assert!(addr.is_vfs_path());
332
333 let addr = AndrAddr("cosmos1...".to_string());
334 assert!(!addr.is_vfs_path());
335 }
336
337 #[test]
338 fn test_is_addr() {
339 let deps = mock_dependencies();
340 let addr = AndrAddr("cosmos1...".to_string());
341 assert!(addr.is_addr(&deps.api));
342 assert!(!addr.is_vfs_path());
343 }
344
345 #[test]
346 fn test_is_local_path() {
347 let addr = AndrAddr("./component".to_string());
348 assert!(addr.is_local_path());
349 assert!(addr.is_vfs_path());
350 }
351
352 #[test]
353 fn test_get_protocol() {
354 let addr = AndrAddr("cosmos1...".to_string());
355 assert!(addr.get_protocol().is_none());
356
357 let addr = AndrAddr("ibc://chain/home/user/app/component".to_string());
358 assert_eq!(addr.get_protocol().unwrap(), "ibc");
359 }
360
361 #[test]
362 fn test_get_chain() {
363 let addr = AndrAddr("cosmos1...".to_string());
364 assert!(addr.get_chain().is_none());
365
366 let addr = AndrAddr("ibc://chain/home/user/app/component".to_string());
367 assert_eq!(addr.get_chain().unwrap(), "chain");
368
369 let addr = AndrAddr("/home/user/app/component".to_string());
370 assert!(addr.get_chain().is_none());
371 }
372
373 #[test]
374 fn test_get_raw_path() {
375 let addr = AndrAddr("cosmos1...".to_string());
376 assert_eq!(addr.get_raw_path(), "cosmos1...");
377
378 let addr = AndrAddr("ibc://chain/home/app/component".to_string());
379 assert_eq!(addr.get_raw_path(), "/home/app/component");
380
381 let addr = AndrAddr("/chain/home/app/component".to_string());
382 assert_eq!(addr.get_raw_path(), "/chain/home/app/component");
383 }
384
385 #[test]
386 fn test_get_root_dir() {
387 let addr = AndrAddr("/home/user1".to_string());
388 assert_eq!(addr.get_root_dir(), "home");
389
390 let addr = AndrAddr("~user1".to_string());
391 assert_eq!(addr.get_root_dir(), "home");
392
393 let addr = AndrAddr("~/user1".to_string());
394 assert_eq!(addr.get_root_dir(), "home");
395
396 let addr = AndrAddr("ibc://chain/home/user1".to_string());
397 assert_eq!(addr.get_root_dir(), "home");
398
399 let addr = AndrAddr("cosmos1...".to_string());
400 assert_eq!(addr.get_root_dir(), "cosmos1...");
401
402 let addr = AndrAddr("./home/user1".to_string());
403 assert_eq!(addr.get_root_dir(), "./home/user1");
404 }
405
406 #[test]
407 fn test_schemars_regex() {
408 let test_cases: Vec<ValidateRegexTestCase> = vec![
409 ValidateRegexTestCase {
410 name: "Normal Path",
411 input: "/home/user",
412 should_err: false,
413 },
414 ValidateRegexTestCase {
415 name: "Path with tilde",
416 input: "~user/dir",
417 should_err: false,
418 },
419 ValidateRegexTestCase {
420 name: "Wrong path with tilde",
421 input: "~/user/dir",
422 should_err: true,
423 },
424 ValidateRegexTestCase {
425 name: "Valid protocol",
426 input: "ibc://chain/home/user/dir",
427 should_err: false,
428 },
429 ValidateRegexTestCase {
430 name: "Valid protocol with tilde",
431 input: "ibc://chain/~user/dir",
432 should_err: false,
433 },
434 ValidateRegexTestCase {
435 name: "Valid Raw Address",
436 input: "cosmos1234567",
437 should_err: false,
438 },
439 ValidateRegexTestCase {
440 name: "Valid Local",
441 input: "./dir/file",
442 should_err: false,
443 },
444 ValidateRegexTestCase {
445 name: "Invalid Local",
446 input: "../dir/file",
447 should_err: true,
448 },
449 ];
450 let re = Regex::new(&ANDR_ADDR_REGEX).unwrap();
451 for test in test_cases {
452 let res = re.is_match(test.input);
453 assert_eq!(!res, test.should_err, "Test case: {}", test.name);
454 }
455 }
456}