1use serde::{Deserialize, Serialize};
8
9use crate::{error::Result, Error, ProjectSlug};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct RemoteSpec {
14 pub origin: String,
16 pub project: ProjectSlug,
18}
19
20#[must_use]
23pub fn strip_reposix_prefix(url: &str) -> &str {
24 let mut stripped = url;
25 while let Some(rest) = stripped.strip_prefix("reposix::") {
26 stripped = rest;
27 }
28 stripped
29}
30
31pub fn split_reposix_url(url: &str) -> Result<(&str, &str)> {
44 let stripped = strip_reposix_prefix(url);
45
46 let Some(idx) = stripped.find("/projects/") else {
47 return Err(Error::InvalidRemote(format!(
48 "expected `/projects/<slug>` in `{stripped}`"
49 )));
50 };
51 let origin = stripped[..idx].trim_end_matches('/');
52 if origin.is_empty() {
53 return Err(Error::InvalidRemote("empty origin".into()));
54 }
55 let tail = &stripped[idx + "/projects/".len()..];
56 let project = tail.trim_end_matches('/');
57 if project.is_empty() {
58 return Err(Error::InvalidRemote(format!(
59 "empty project segment in `{stripped}`"
60 )));
61 }
62 Ok((origin, project))
63}
64
65pub fn parse_remote_url(url: &str) -> Result<RemoteSpec> {
71 let (origin, project_tail) = split_reposix_url(url)?;
72 let slug_str = project_tail.split('/').next().unwrap_or("");
78 let project = ProjectSlug::parse(slug_str)
79 .ok_or_else(|| Error::InvalidRemote(format!("invalid project slug: `{slug_str}`")))?;
80 Ok(RemoteSpec {
81 origin: origin.to_owned(),
82 project,
83 })
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn parses_with_prefix() {
92 let r = parse_remote_url("reposix::http://localhost:7777/projects/demo").unwrap();
93 assert_eq!(r.origin, "http://localhost:7777");
94 assert_eq!(r.project.as_str(), "demo");
95 }
96
97 #[test]
98 fn parses_without_prefix() {
99 let r = parse_remote_url("https://api.example.com/projects/PROJ-A").unwrap();
100 assert_eq!(r.origin, "https://api.example.com");
101 assert_eq!(r.project.as_str(), "PROJ-A");
102 }
103
104 #[test]
105 fn rejects_path_traversal_slug() {
106 assert!(parse_remote_url("http://x/projects/..").is_err());
107 }
108}