1use crate::{ado_base::ownership::OwnershipMessage, amp::AndrAddr, error::ContractError};
2use cosmwasm_schema::{cw_serde, QueryResponses};
3use cosmwasm_std::{ensure, Addr, Api, QuerierWrapper};
4use regex::Regex;
5
6pub const COMPONENT_NAME_REGEX: &str = r"^[A-Za-z0-9.\-_]{2,80}$";
7pub const USERNAME_REGEX: &str = r"^[a-z0-9]{2,30}$";
8
9pub const PATH_REGEX: &str = r"^(~[a-z0-9]{2,}|/(lib|home))(/[A-Za-z0-9.\-_]{2,80}?)*(/)?$";
10pub const PROTOCOL_PATH_REGEX: &str = r"^((([A-Za-z0-9]+://)?([A-Za-z0-9.\-_]{2,80}/)))?((~[a-z0-9]{2,}|(lib|home))(/[A-Za-z0-9.\-_]{2,80}?)*(/)?)$";
11
12pub fn convert_component_name(path: &str) -> String {
13 path.trim()
14 .replace(' ', "_")
15 .chars()
16 .filter(|c| c.is_alphanumeric() || *c == '.' || *c == '-' || *c == '_')
17 .collect::<String>()
18 .to_lowercase()
19}
20
21pub fn validate_component_name(path: String) -> Result<bool, ContractError> {
22 ensure!(
23 path.chars().any(|c| c.is_alphanumeric()),
24 ContractError::InvalidPathname {
25 error: Some("Pathname must contain at least one alphanumeric character".to_string())
26 }
27 );
28 let re = Regex::new(COMPONENT_NAME_REGEX).unwrap();
29
30 ensure!(
31 re.is_match(&path),
32 ContractError::InvalidPathname {
33 error: Some("Pathname includes an invalid character".to_string())
34 }
35 );
36
37 Ok(true)
38}
39
40pub fn validate_username(username: String) -> Result<bool, ContractError> {
64 ensure!(
66 !username.is_empty(),
67 ContractError::InvalidUsername {
68 error: Some("Username cannot be empty.".to_string())
69 }
70 );
71
72 let re = Regex::new(USERNAME_REGEX).unwrap();
74 ensure!(
76 re.is_match(&username),
77 ContractError::InvalidPathname {
78 error: Some(
79 "Username contains invalid characters. All characters must be alphanumeric."
80 .to_string()
81 )
82 }
83 );
84 Ok(true)
86}
87
88pub fn validate_path_name(api: &dyn Api, path: String) -> Result<(), ContractError> {
89 let andr_addr = AndrAddr::from_string(path.clone());
90 let is_path_reference = path.contains('/');
91 let includes_protocol = andr_addr.get_protocol().is_some();
92
93 if is_path_reference {
95 let regex_str = if includes_protocol {
97 PROTOCOL_PATH_REGEX
98 } else {
99 PATH_REGEX
100 };
101
102 let re = Regex::new(regex_str).unwrap();
103 ensure!(
104 re.is_match(&path),
105 ContractError::InvalidPathname {
106 error: Some("Pathname includes an invalid character".to_string())
107 }
108 );
109
110 return Ok(());
111 }
112
113 if !is_path_reference {
115 let path = path.strip_prefix('~').unwrap_or(&path);
116 let is_address = api.addr_validate(path).is_ok();
117
118 if is_address {
119 return Ok(());
120 }
121
122 let is_username = validate_username(path.to_string()).is_ok();
123
124 if is_username {
125 return Ok(());
126 }
127
128 return Err(ContractError::InvalidPathname {
129 error: Some(
130 "Provided address is neither a valid username nor a valid address".to_string(),
131 ),
132 });
133 }
134
135 Err(ContractError::InvalidPathname { error: None })
137}
138
139#[cw_serde]
140pub struct InstantiateMsg {
141 pub kernel_address: String,
143 pub owner: Option<String>,
144}
145
146#[cw_serde]
147pub struct PathDetails {
148 name: String,
149 address: Addr,
150}
151
152impl PathDetails {
153 pub fn new(name: impl Into<String>, address: Addr) -> PathDetails {
154 PathDetails {
155 name: name.into(),
156 address,
157 }
158 }
159}
160
161#[cw_serde]
162pub enum ExecuteMsg {
163 AddPath {
164 #[schemars(regex = "COMPONENT_NAME_REGEX")]
165 name: String,
166 address: Addr,
167 parent_address: Option<AndrAddr>,
168 },
169 AddSymlink {
170 #[schemars(regex = "COMPONENT_NAME_REGEX")]
171 name: String,
172 symlink: AndrAddr,
173 parent_address: Option<AndrAddr>,
174 },
175 AddChild {
177 #[schemars(regex = "COMPONENT_NAME_REGEX")]
178 name: String,
179 parent_address: AndrAddr,
180 },
181 RegisterUser {
182 #[schemars(regex = "USERNAME_REGEX", length(min = 3, max = 30))]
183 username: String,
184 address: Option<Addr>,
185 },
186 RegisterLibrary {
188 lib_name: String,
189 lib_address: Addr,
190 },
191 RegisterUserCrossChain {
192 chain: String,
193 address: String,
194 },
195 Ownership(OwnershipMessage),
197}
198
199#[cw_serde]
200pub struct SubDirBound {
201 address: Addr,
202 name: String,
203}
204impl From<SubDirBound> for (Addr, String) {
205 fn from(val: SubDirBound) -> Self {
206 (val.address, val.name)
207 }
208}
209
210#[cw_serde]
211#[derive(QueryResponses)]
212pub enum QueryMsg {
213 #[returns(Addr)]
214 ResolvePath { path: AndrAddr },
215 #[returns(Vec<PathDetails>)]
216 SubDir {
217 path: AndrAddr,
218 min: Option<SubDirBound>,
219 max: Option<SubDirBound>,
220 limit: Option<u32>,
221 },
222 #[returns(Vec<String>)]
223 Paths { addr: Addr },
224 #[returns(String)]
225 GetUsername { address: Addr },
226 #[returns(String)]
227 GetLibrary { address: Addr },
228 #[returns(AndrAddr)]
229 ResolveSymlink { path: AndrAddr },
230 #[returns(crate::ado_base::version::VersionResponse)]
232 Version {},
233 #[returns(crate::ado_base::ado_type::TypeResponse)]
234 Type {},
235 #[returns(crate::ado_base::ownership::ContractOwnerResponse)]
236 Owner {},
237 #[returns(crate::ado_base::kernel_address::KernelAddressResponse)]
238 KernelAddress {},
239}
240
241pub fn vfs_resolve_path(
243 path: impl Into<String>,
244 vfs_contract: impl Into<String>,
245 querier: &QuerierWrapper,
246) -> Result<Addr, ContractError> {
247 let query = QueryMsg::ResolvePath {
248 path: AndrAddr::from_string(path.into()),
249 };
250 let addr = querier.query_wasm_smart::<Addr>(vfs_contract, &query);
251 match addr {
252 Ok(addr) => Ok(addr),
253 Err(_) => Err(ContractError::InvalidAddress {}),
254 }
255}
256
257pub fn vfs_resolve_symlink(
259 path: impl Into<String>,
260 vfs_contract: impl Into<String>,
261 querier: &QuerierWrapper,
262) -> Result<AndrAddr, ContractError> {
263 let query = QueryMsg::ResolveSymlink {
264 path: AndrAddr::from_string(path.into()),
265 };
266 let addr = querier.query_wasm_smart::<AndrAddr>(vfs_contract, &query)?;
267 Ok(addr)
268}
269
270#[cfg(test)]
271mod test {
272 use cosmwasm_std::testing::mock_dependencies;
273
274 use super::*;
275
276 struct ValidateComponentNameTestCase {
277 name: &'static str,
278 input: &'static str,
279 should_err: bool,
280 }
281
282 #[test]
283 fn test_validate_component_name() {
284 let test_cases: Vec<ValidateComponentNameTestCase> = vec![
285 ValidateComponentNameTestCase {
286 name: "standard component name",
287 input: "component1",
288 should_err: false
289 },
290 ValidateComponentNameTestCase {
291 name: "component with hyphen",
292 input: "component-2",
293 should_err: false,
294 },
295 ValidateComponentNameTestCase {
296 name: "component with underscore",
297 input: "component_2",
298 should_err: false,
299 },
300 ValidateComponentNameTestCase {
301 name: "component with period",
302 input: ".component2",
303 should_err: false,
304 },
305 ValidateComponentNameTestCase {
306 name: "component with invalid character",
307 input: "component$2",
308 should_err: true,
309 },
310 ValidateComponentNameTestCase {
311 name: "component with spaces",
312 input: "component 2",
313 should_err: true,
314 },
315 ValidateComponentNameTestCase {
316 name: "empty component name",
317 input: "",
318 should_err: true,
319 },
320 ValidateComponentNameTestCase {
321 name: "component name too long",
322 input: "somereallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallylongname",
323 should_err: true,
324 },
325 ValidateComponentNameTestCase {
326 name: "component name with only special characters",
327 input: "!@#$%^&*()",
328 should_err: true,
329 },
330 ValidateComponentNameTestCase {
331 name: "component name with leading and trailing spaces",
332 input: " component2 ",
333 should_err: true,
334 },
335 ValidateComponentNameTestCase {
336 name: "component name with only numbers",
337 input: "123456",
338 should_err: false,
339 },
340 ValidateComponentNameTestCase {
341 name: "component name one letter",
342 input: "a",
343 should_err: true,
344 },
345 ValidateComponentNameTestCase {
346 name: "component name two letters",
347 input: "ab",
348 should_err: false,
349 },
350 ValidateComponentNameTestCase {
351 name: "component with hyphen at the start",
352 input: "-component-2",
353 should_err: false,
354 },
355 ValidateComponentNameTestCase {
356 name: "component with forward slash",
357 input: "component-2/",
358 should_err: true,
359 },
360 ValidateComponentNameTestCase {
361 name: "component with backward slash",
362 input: r"component-2\",
363 should_err: true,
364 },
365 ValidateComponentNameTestCase {
366 name: "component name with upper case letters",
367 input: "ComponentName",
368 should_err: false,
369 }
370 ];
371
372 for test in test_cases {
373 let res = validate_component_name(test.input.to_string());
374 assert_eq!(res.is_err(), test.should_err, "Test case: {}", test.name);
375 }
376 }
377
378 struct ValidatePathNameTestCase {
379 name: &'static str,
380 path: &'static str,
381 should_err: bool,
382 }
383
384 #[test]
385 fn test_validate_path_name() {
386 let test_cases: Vec<ValidatePathNameTestCase> = vec![
387 ValidatePathNameTestCase {
388 name: "Simple app path",
389 path: "./username/app",
390 should_err: true,
391 },
392 ValidatePathNameTestCase {
393 name: "Root path",
394 path: "/",
395 should_err: true,
396 },
397 ValidatePathNameTestCase {
398 name: "Relative path with parent directory",
399 path: "../username/app",
400 should_err: true,
401 },
402 ValidatePathNameTestCase {
403 name: "Tilde username reference",
404 path: "~usr",
406 should_err: false,
407 },
408 ValidatePathNameTestCase {
409 name: "Tilde address reference",
410 path: "~cosmos1abcde",
411 should_err: false,
412 },
413 ValidatePathNameTestCase {
414 name: "Tilde username reference with directory",
415 path: "~usr/app/splitter",
416 should_err: false,
417 },
418 ValidatePathNameTestCase {
419 name: "Invalid tilde username reference",
420 path: "~/username",
421 should_err: true,
422 },
423 ValidatePathNameTestCase {
424 name: "Absolute path with tilde",
425 path: "~/home/username",
426 should_err: true,
427 },
428 ValidatePathNameTestCase {
429 name: "Invalid user path",
430 path: "/user/un",
431 should_err: true,
432 },
433 ValidatePathNameTestCase {
434 name: "Valid user path",
435 path: "/home/usr",
436 should_err: false,
437 },
438 ValidatePathNameTestCase {
439 name: "Invalid home path (address)",
440 path: "/user/cosmos1abcde",
441 should_err: true,
442 },
443 ValidatePathNameTestCase {
444 name: "Valid home path (address)",
445 path: "/home/cosmos1abcde",
446 should_err: false,
447 },
448 ValidatePathNameTestCase {
449 name: "Valid lib path",
450 path: "/lib/library",
451 should_err: false,
452 },
453 ValidatePathNameTestCase {
454 name: "Complex invalid path",
455 path: "/home/username/dir1/../dir2/./file",
456 should_err: true,
457 },
458 ValidatePathNameTestCase {
459 name: "Path with invalid characters",
460 path: "/home/username/dir1/|file",
461 should_err: true,
462 },
463 ValidatePathNameTestCase {
464 name: "Path with space",
465 path: "/home/ username/dir1/file",
466 should_err: true,
467 },
468 ValidatePathNameTestCase {
469 name: "Empty path",
470 path: "",
471 should_err: true,
472 },
473 ValidatePathNameTestCase {
474 name: "Path with only special characters",
475 path: "///",
476 should_err: true,
477 },
478 ValidatePathNameTestCase {
479 name: "Path with only special characters and spaces",
480 path: "/// / /// //",
481 should_err: true,
482 },
483 ValidatePathNameTestCase {
484 name: "Valid ibc protocol path",
485 path: "ibc://chain/home/username/dir1/file",
486 should_err: false,
487 },
488 ValidatePathNameTestCase {
489 name: "Invalid ibc protocol path",
490 path: "ibc:///home/username/dir1/file",
491 should_err: true,
492 },
493 ValidatePathNameTestCase {
494 name: "Standard address",
495 path: "cosmos1abcde",
496 should_err: false,
497 },
498 ValidatePathNameTestCase {
499 name: "Only periods",
500 path: "/../../../..",
501 should_err: true,
502 },
503 ValidatePathNameTestCase {
504 name: "Path with newline character",
505 path: "/home/username/dir1\n/file",
506 should_err: true,
507 },
508 ValidatePathNameTestCase {
509 name: "Path with tab character",
510 path: "/home/username/dir1\t/dir2",
511 should_err: true,
512 },
513 ValidatePathNameTestCase {
514 name: "Path with null character",
515 path: "/home/username\0/dir1",
516 should_err: true,
517 },
518 ValidatePathNameTestCase {
519 name: "Path with emoji",
520 path: "/home/username/😊",
521 should_err: true,
522 },
523 ValidatePathNameTestCase {
524 name: "Path with Cyrillic characters",
525 path: "/home/пользователь/dir1",
526 should_err: true,
527 },
528 ValidatePathNameTestCase {
529 name: "Path with Arabic characters",
530 path: "/home/مستخدم/dir1",
531 should_err: true,
532 },
533 ValidatePathNameTestCase {
534 name: "Path with Chinese characters",
535 path: "/home/用户/dir1",
536 should_err: true,
537 },
538 ValidatePathNameTestCase {
539 name: "Path with very long name",
540 path: "/home/username/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
541 should_err: true,
542 },
543 ValidatePathNameTestCase {
544 name: "Valid path with multiple subdirectories",
545 path: "/home/username/dir1/dir2/dir3/dir4",
546 should_err: false,
547 },
548 ValidatePathNameTestCase {
549 name: "Path with unprintable ASCII character",
550 path: "/home/username/\x07file",
551 should_err: true,
552 },
553 ];
566
567 for test in test_cases {
568 let deps = mock_dependencies();
569 let res = validate_path_name(&deps.api, test.path.to_string());
570 assert_eq!(res.is_err(), test.should_err, "Test case: {}", test.name);
571 }
572 }
573
574 struct ConvertComponentNameTestCase {
575 name: &'static str,
576 input: &'static str,
577 expected: &'static str,
578 }
579
580 #[test]
581 fn test_convert_component_name() {
582 let test_cases: Vec<ConvertComponentNameTestCase> = vec![
583 ConvertComponentNameTestCase {
584 name: "Standard name with spaces",
585 input: "Some Component Name",
586 expected: "some_component_name",
587 },
588 ConvertComponentNameTestCase {
589 name: "Name with hyphens",
590 input: "Some-Component-Name",
591 expected: "some-component-name",
592 },
593 ConvertComponentNameTestCase {
594 name: "Name with uppercase letters",
595 input: "SomeCOMPONENTName",
596 expected: "somecomponentname",
597 },
598 ConvertComponentNameTestCase {
599 name: "Name with numbers",
600 input: "Component123",
601 expected: "component123",
602 },
603 ConvertComponentNameTestCase {
604 name: "Name with special characters",
605 input: "Component!@#",
606 expected: "component",
607 },
608 ConvertComponentNameTestCase {
609 name: "Empty name",
610 input: "",
611 expected: "",
612 },
613 ConvertComponentNameTestCase {
614 name: "Name with leading and trailing spaces",
615 input: " Some Component Name ",
616 expected: "some_component_name",
617 },
618 ConvertComponentNameTestCase {
619 name: "Name with multiple spaces",
620 input: "Some Component Name",
621 expected: "some____component____name",
622 },
623 ];
624
625 for test in test_cases {
626 assert_eq!(
627 convert_component_name(test.input),
628 test.expected,
629 "Test case: {}",
630 test.name
631 )
632 }
633 }
634
635 struct ValidateUsernameTestCase {
636 name: &'static str,
637 username: &'static str,
638 should_err: bool,
639 }
640
641 #[test]
642 fn test_validate_username() {
643 let test_cases: Vec<ValidateUsernameTestCase> = vec![
644 ValidateUsernameTestCase {
645 name: "Valid lowercase username",
646 username: "validusername",
647 should_err: false,
648 },
649 ValidateUsernameTestCase {
650 name: "Valid numeric username",
651 username: "123456",
652 should_err: false,
653 },
654 ValidateUsernameTestCase {
655 name: "Username with uppercase letters",
656 username: "InvalidUsername",
657 should_err: true,
658 },
659 ValidateUsernameTestCase {
660 name: "Username with special characters",
661 username: "user!@#",
662 should_err: true,
663 },
664 ValidateUsernameTestCase {
665 name: "Empty username",
666 username: "",
667 should_err: true,
668 },
669 ValidateUsernameTestCase {
670 name: "Username with underscore",
671 username: "valid_username",
672 should_err: true,
673 },
674 ValidateUsernameTestCase {
675 name: "Username with hyphen",
676 username: "valid-username",
677 should_err: true,
678 },
679 ValidateUsernameTestCase {
680 name: "Username with period",
681 username: "valid.username",
682 should_err: true,
683 },
684 ValidateUsernameTestCase {
685 name: "Username with leading numbers",
686 username: "123validusername",
687 should_err: false,
688 },
689 ValidateUsernameTestCase {
690 name: "Username with only three characters",
691 username: "usr",
692 should_err: false,
693 },
694 ValidateUsernameTestCase {
695 name: "Username with only one character",
696 username: "a",
697 should_err: true,
698 },
699 ValidateUsernameTestCase {
700 name: "Username with whitespace",
701 username: "valid username",
702 should_err: true,
703 },
704 ValidateUsernameTestCase {
705 name: "Username with leading whitespace",
706 username: " validusername",
707 should_err: true,
708 },
709 ValidateUsernameTestCase {
710 name: "Username with trailing whitespace",
711 username: "validusername ",
712 should_err: true,
713 },
714 ValidateUsernameTestCase {
715 name: "Username with mixed case letters",
716 username: "ValidUserName",
717 should_err: true,
718 },
719 ValidateUsernameTestCase {
720 name: "Username with all uppercase letters",
721 username: "VALIDUSERNAME",
722 should_err: true,
723 },
724 ];
725
726 for test in test_cases {
727 assert_eq!(
728 validate_username(test.username.to_string()).is_err(),
729 test.should_err,
730 "Test case: {}",
731 test.name
732 )
733 }
734 }
735}