andromeda_std/os/
vfs.rs

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
40/// Validates a username against specific criteria.
41///
42/// This function checks if a given username meets the following conditions:
43/// - It must contain at least three characters
44/// - It must only contain alphanumeric characters
45///
46/// # Arguments
47///
48/// * `username` - A `String` representing the username to be validated.
49///
50/// # Returns
51///
52/// * `Result<bool, ContractError>` - Returns `Ok(true)` if the username is valid, otherwise returns an `Err` with a `ContractError` detailing the reason for invalidity.
53///
54/// # Examples
55///
56/// ```
57//// let valid_username = validate_username("validuser123".to_string()).unwrap();
58//// assert_eq!(valid_username, true);
59///
60//// let invalid_username = validate_username("".to_string()).is_err();
61//// assert!(invalid_username);
62/// ```
63pub fn validate_username(username: String) -> Result<bool, ContractError> {
64    // Ensure the username is not empty.
65    ensure!(
66        !username.is_empty(),
67        ContractError::InvalidUsername {
68            error: Some("Username cannot be empty.".to_string())
69        }
70    );
71
72    // Compile the regex for validating alphanumeric characters.
73    let re = Regex::new(USERNAME_REGEX).unwrap();
74    // Ensure the username matches the alphanumeric regex pattern.
75    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    // Return true if all validations pass.
85    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    // Path is of the form /user/... or /lib/... or prot://...
94    if is_path_reference {
95        // Alter regex if path includes a protocol
96        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    // Path is either a username or address
114    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    // Does not fit any valid conditions
136    Err(ContractError::InvalidPathname { error: None })
137}
138
139#[cw_serde]
140pub struct InstantiateMsg {
141    /// Address of the Kernel contract on chain
142    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    // Registers a child, currently only accessible by an App Contract
176    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    // Restricted to VFS owner/Kernel
187    RegisterLibrary {
188        lib_name: String,
189        lib_address: Addr,
190    },
191    RegisterUserCrossChain {
192        chain: String,
193        address: String,
194    },
195    // Base message
196    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    // Base queries
231    #[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
241/// Queries the provided VFS contract address to resolve the given path
242pub 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
257/// Queries the provided VFS contract address to resolve the given path
258pub 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                // Username must be short to circumvent it being mistaken as an address
405                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            // This case should fail but due to the restriction of mock dependencies we cannot validate it correctly! It is partially validated in test_validate_username
554            // ValidatePathNameTestCase {
555            //     name: "Really long username",
556            //     path: "~somereallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallylongname",
557            //     should_err: true,
558            // },
559            // This case should fail but due to the restriction of mock dependencies we cannot validate it correctly!
560            // ValidatePathNameTestCase {
561            //     name: "Standard address with backslash",
562            //     path: r"\cosmos\1abcde\",
563            //     should_err: true,
564            // },
565        ];
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}