Skip to main content

lfsx_server/
namespace.rs

1use crate::error::Error;
2
3const FORGE_SEPARATOR: char = '~';
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct Namespace {
7    forge: Option<String>,
8    org: String,
9    repo: String,
10    stored_org: String,
11}
12
13impl Namespace {
14    pub fn new(org: impl Into<String>, repo: impl Into<String>) -> Result<Self, Error> {
15        let (org, repo) = (org.into(), repo.into());
16
17        (is_well_formed(&org) && is_well_formed(&repo))
18            .then(|| Self {
19                forge: None,
20                stored_org: org.clone(),
21                org,
22                repo,
23            })
24            .ok_or(Error::MalformedNamespace)
25    }
26
27    pub fn on(
28        forge: impl Into<String>,
29        org: impl Into<String>,
30        repo: impl Into<String>,
31    ) -> Result<Self, Error> {
32        let forge = forge.into();
33        if !is_forge_name(&forge) {
34            return Err(Error::MalformedNamespace);
35        }
36        let ns = Self::new(org, repo)?;
37
38        Ok(Self {
39            stored_org: format!("{forge}{FORGE_SEPARATOR}{}", ns.org),
40            forge: Some(forge),
41            ..ns
42        })
43    }
44
45    pub fn forge(&self) -> Option<&str> {
46        self.forge.as_deref()
47    }
48
49    pub fn org(&self) -> &str {
50        &self.org
51    }
52
53    pub fn repo(&self) -> &str {
54        &self.repo
55    }
56
57    pub fn stored_org(&self) -> &str {
58        &self.stored_org
59    }
60
61    pub fn upstream(&self) -> String {
62        format!("{}/{}", self.org, self.repo)
63    }
64
65    pub fn url_path(&self) -> String {
66        match &self.forge {
67            Some(forge) => format!("-/{forge}/{}/{}", self.org, self.repo),
68            None => self.upstream(),
69        }
70    }
71}
72
73impl std::fmt::Display for Namespace {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(f, "{}/{}", self.stored_org, self.repo)
76    }
77}
78
79pub fn is_forge_name(name: &str) -> bool {
80    (1..=32).contains(&name.len())
81        && name.starts_with(|c: char| c.is_ascii_lowercase() || c.is_ascii_digit())
82        && name
83            .bytes()
84            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
85        && !matches!(name, "api" | "dashboard")
86}
87
88fn is_well_formed(segment: &str) -> bool {
89    !segment.is_empty()
90        && segment.len() <= 100
91        && !segment.starts_with('.')
92        && segment
93            .bytes()
94            .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
95}
96
97#[cfg(test)]
98mod tests;