ghactions_core/repository/
reference.rs

1//! RepositoryReference is a struct that holds the owner, name, path and reference of a repository
2//!
3#![allow(unused_assignments)]
4use std::{
5    fmt::Display,
6    path::{Component, PathBuf},
7};
8
9use crate::ActionsError;
10
11/// RepositoryReference is a struct that holds the owner, name, path and reference of a repository
12#[derive(Debug, Default, Clone, PartialEq, Eq)]
13pub struct RepositoryReference {
14    /// Repository owner
15    pub owner: String,
16    /// Repository name
17    pub name: String,
18    /// Repository path
19    pub path: Option<String>,
20    /// Repository reference / branch
21    pub reference: Option<String>,
22}
23
24impl RepositoryReference {
25    /// Parse a repository reference
26    ///
27    /// Example:
28    /// ```
29    /// use ghactions_core::RepositoryReference;
30    ///
31    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
32    /// let reference = "geekmasher/ghaction@main";
33    /// let reporef = RepositoryReference::parse(reference)?;
34    ///
35    /// println!("Owner: {}", reporef.owner);
36    /// println!("Name: {}", reporef.name);
37    /// println!("Reference (optional): {:#?}", reporef.reference);
38    /// println!("Path (optional): {:#?}", reporef.path);
39    /// # Ok(())
40    /// # }
41    /// ```
42    pub fn parse(reporef: &str) -> Result<RepositoryReference, ActionsError> {
43        let mut repo_ref = RepositoryReference::default();
44
45        let mut repository = String::new();
46        let mut path = PathBuf::new();
47
48        match reporef.split_once('@') {
49            Some((repo, refe)) => {
50                repository = repo.to_string();
51                repo_ref.reference = Some(refe.to_string());
52            }
53            None => {
54                repository = reporef.to_string();
55            }
56        }
57
58        // split up the rest for the following
59        // first: owner, second: repo, third+ : path
60        for (index, path_ref) in repository.split('/').enumerate() {
61            if index == 0 {
62                repo_ref.owner = path_ref.to_string();
63            } else if index == 1 {
64                repo_ref.name = path_ref.to_string();
65            } else {
66                path.push(path_ref);
67            }
68        }
69
70        // If the path is now empty, create the full path
71        if !path.as_os_str().is_empty() {
72            // This is a basic way to detect path traversal, might want to do better
73            if path.components().any(|x| x == Component::ParentDir) {
74                return Err(ActionsError::RepositoryReferenceError(
75                    "Path traversal detected".to_string(),
76                ));
77            }
78            if path.is_absolute() {
79                return Err(ActionsError::RepositoryReferenceError(
80                    "Absolute paths are not allowed".to_string(),
81                ));
82            }
83            repo_ref.path = Some(path.display().to_string());
84        }
85
86        Ok(repo_ref)
87    }
88
89    /// Covert the RepositoryReference to a displayable string
90    pub fn display(&self) -> String {
91        format!("{}", self)
92    }
93}
94
95impl Display for RepositoryReference {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        let mut retvalue = format!("{}/{}", self.owner, self.name);
98        if let Some(path) = &self.path {
99            retvalue.push('/');
100            retvalue.push_str(path.as_str());
101        }
102        if let Some(refer) = &self.reference {
103            retvalue.push('@');
104            retvalue.push_str(refer.as_str());
105        }
106
107        write!(f, "{}", retvalue)
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::RepositoryReference;
114
115    #[test]
116    fn test_owner_repo() {
117        let repo_ref = RepositoryReference::parse("geekmasher/ghactions").unwrap();
118
119        assert_eq!(repo_ref.owner, String::from("geekmasher"));
120        assert_eq!(repo_ref.name, String::from("ghactions"));
121        assert_eq!(repo_ref.reference, None);
122        assert_eq!(repo_ref.path, None);
123
124        assert_eq!(repo_ref.display(), String::from("geekmasher/ghactions"));
125    }
126    #[test]
127    fn test_owner_repo_branch() {
128        let repo_ref = RepositoryReference::parse("geekmasher/ghactions@main").unwrap();
129
130        assert_eq!(repo_ref.owner, String::from("geekmasher"));
131        assert_eq!(repo_ref.name, String::from("ghactions"));
132        assert_eq!(repo_ref.reference, Some(String::from("main")));
133        assert_eq!(repo_ref.path, None);
134
135        assert_eq!(
136            repo_ref.display(),
137            String::from("geekmasher/ghactions@main")
138        );
139    }
140    #[test]
141    fn test_owner_repo_branch_path() {
142        let repo_ref = RepositoryReference::parse("geekmasher/ghactions@feature/xyz").unwrap();
143
144        assert_eq!(repo_ref.owner, String::from("geekmasher"));
145        assert_eq!(repo_ref.name, String::from("ghactions"));
146        assert_eq!(repo_ref.reference, Some(String::from("feature/xyz")));
147        assert_eq!(repo_ref.path, None);
148
149        assert_eq!(
150            repo_ref.display(),
151            String::from("geekmasher/ghactions@feature/xyz")
152        );
153    }
154    #[test]
155    fn test_owner_repo_path() {
156        let repo_ref =
157            RepositoryReference::parse("geekmasher/ghactions/path/to/action@main").unwrap();
158
159        assert_eq!(repo_ref.owner, String::from("geekmasher"));
160        assert_eq!(repo_ref.name, String::from("ghactions"));
161        assert_eq!(repo_ref.reference, Some(String::from("main")));
162        assert_eq!(repo_ref.path, Some(String::from("path/to/action")));
163
164        assert_eq!(
165            repo_ref.display(),
166            String::from("geekmasher/ghactions/path/to/action@main")
167        );
168    }
169    #[test]
170    fn test_owner_repo_path_traversal() {
171        let repo_ref = RepositoryReference::parse("geekmasher/ghactions/../test@main");
172        assert!(repo_ref.is_err());
173
174        // TODO
175        // let repo_ref = RepositoryReference::parse("geekmasher/ghaction/%2E%2E/test@main");
176        // assert!(repo_ref.is_err());
177    }
178}