Skip to content
Clone
a06673b
131 lines 4.1 KB Rust
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use uuid::Uuid;
use crate::config::SharedRepoConfig;
use crate::error::{RepoError, RepoResult};
pub const DEFAULT_OBJECT_FORMAT_SHA1: &str = "sha1";
pub const DEFAULT_OBJECT_FORMAT_SHA256: &str = "sha256";
pub fn validate_repo_name(name: &str, max_len: usize) -> RepoResult<String> {
let trimmed = name.trim();
if trimmed.is_empty() {
return Err(RepoError::BadRequest("repository name is empty".into()));
}
if trimmed.len() > max_len {
return Err(RepoError::BadRequest(format!(
"repository name too long (max {max_len})"
)));
}
if !trimmed
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
{
return Err(RepoError::BadRequest(
"repository name must match [A-Za-z0-9._-]".into(),
));
}
if trimmed.starts_with('.') || trimmed.starts_with('-') {
return Err(RepoError::BadRequest(
"repository name may not start with '.' or '-'".into(),
));
}
Ok(trimmed.to_lowercase())
}
pub fn validate_branch_name(name: &str, max_len: usize) -> RepoResult<String> {
let trimmed = name.trim();
if trimmed.is_empty() || trimmed.len() > max_len {
return Err(RepoError::BadRequest("invalid branch name".into()));
}
if trimmed.starts_with('-') || trimmed.starts_with('.') {
return Err(RepoError::BadRequest("invalid branch name prefix".into()));
}
if trimmed.contains("..") || trimmed.contains("@{") || trimmed.contains('\\') {
return Err(RepoError::BadRequest("invalid branch name".into()));
}
Ok(trimmed.to_string())
}
pub fn validate_tag_name(name: &str, max_len: usize) -> RepoResult<String> {
let trimmed = name.trim();
if trimmed.is_empty() || trimmed.len() > max_len {
return Err(RepoError::BadRequest("invalid tag name".into()));
}
if trimmed.contains("..") || trimmed.contains(' ') {
return Err(RepoError::BadRequest("invalid tag name".into()));
}
Ok(trimmed.to_string())
}
pub fn validate_object_format(fmt: &str) -> RepoResult<&'static str> {
match fmt {
"sha1" => Ok(DEFAULT_OBJECT_FORMAT_SHA1),
"sha256" => Ok(DEFAULT_OBJECT_FORMAT_SHA256),
_ => Err(RepoError::BadRequest(format!(
"unsupported object format: {fmt}"
))),
}
}
pub fn git_bare_path(config: &SharedRepoConfig, channel_id: Uuid, repo_id: Uuid) -> PathBuf {
Path::new(&config.git_storage_root)
.join(channel_id.to_string())
.join(format!("{repo_id}.git"))
}
pub fn lfs_path(config: &SharedRepoConfig, channel_id: Uuid, repo_id: Uuid) -> PathBuf {
Path::new(&config.lfs_storage_root)
.join(channel_id.to_string())
.join(repo_id.to_string())
}
pub fn parse_owner_repo_slug(slug: &str) -> RepoResult<(&str, &str)> {
let (space, repo) = slug
.split_once('/')
.ok_or_else(|| RepoError::BadRequest("expected '<space_name>/<repo_name>'".into()))?;
if space.is_empty() || repo.is_empty() {
return Err(RepoError::BadRequest("empty space or repo segment".into()));
}
Ok((space, repo))
}
pub fn normalize_topic(topic: &str) -> RepoResult<String> {
let t = topic.trim().to_lowercase();
if t.is_empty() {
return Err(RepoError::BadRequest("empty topic".into()));
}
if t.len() > 35 {
return Err(RepoError::BadRequest("topic too long".into()));
}
if !t
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
{
return Err(RepoError::BadRequest("invalid topic characters".into()));
}
Ok(t)
}
pub fn sanitize_topics<I, S>(topics: I, max: usize) -> RepoResult<Vec<String>>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut seen = HashSet::new();
let mut out = Vec::new();
for t in topics {
let n = normalize_topic(t.as_ref())?;
if seen.insert(n.clone()) {
out.push(n);
}
}
if out.len() > max {
return Err(RepoError::BadRequest(format!(
"too many topics (max {max})"
)));
}
Ok(out)
}