don't
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(knot): consolidate `RepoId` and `RepoPath`

Signed-off-by: tjh <did:plc:65gha4t3avpfpzmvpbwovss7>

+93 -69
+1 -1
crates/knot/src/lib.rs
··· 7 7 pub use objectid::ObjectId; 8 8 9 9 mod repoid; 10 - pub use repoid::RepoId; 10 + pub use repoid::RepoPath;
+2 -2
crates/knot/src/main.rs
··· 9 9 rt::TokioExecutor, 10 10 }; 11 11 use identity::Resolver; 12 - use knot::{RepoId, model::Knot}; 12 + use knot::{RepoPath, model::Knot}; 13 13 use reqwest::StatusCode; 14 14 use serde::Deserialize; 15 15 use std::{ ··· 45 45 46 46 #[derive(Deserialize)] 47 47 struct Repo { 48 - repo: RepoId, 48 + repo: RepoPath, 49 49 } 50 50 51 51 fn extract_request_id<B>(request: &Request<B>) -> Option<&str> {
+6 -7
crates/knot/src/model.rs
··· 1 1 use crate::{ 2 - RepoId, 2 + RepoPath, 3 3 convert::time_to_offsetdatetime, 4 4 model::errors::{HeadDetached, PathNotFound, RefNotFound, RepoEmpty, RepoError, RepoNotFound}, 5 5 public::xrpc::XrpcError, 6 - repoid::RepoParts as _, 7 6 types::sh::tangled::repo::{ 8 7 BlobEncoding, BlobParams, BlobResponse, Branch, BranchesParams, BranchesResponse, 9 8 DefaultBranchResponse, Diff, DiffParams, DiffResponse, GetDefaultBranchParams, JsonBlob, ··· 57 58 struct KnotState { 58 59 owner: Did, 59 60 repository_base: PathBuf, 60 - repository_cache: Mutex<FxHashMap<RepoId, ThreadSafeRepository>>, 61 + repository_cache: Mutex<FxHashMap<RepoPath, ThreadSafeRepository>>, 61 62 } 62 63 63 64 pub trait RepositoryManager { 64 - fn open(&self, repo: &RepoId) -> Result<Repository, XrpcError>; 65 + fn open(&self, repo: &RepoPath) -> Result<Repository, XrpcError>; 65 66 66 - fn reopen(&self, repo: &RepoId) -> Result<Repository, XrpcError>; 67 + fn reopen(&self, repo: &RepoPath) -> Result<Repository, XrpcError>; 67 68 } 68 69 69 70 impl RepositoryManager for Knot { 70 - fn open(&self, repo: &RepoId) -> Result<Repository, XrpcError> { 71 + fn open(&self, repo: &RepoPath) -> Result<Repository, XrpcError> { 71 72 let mut cache = self 72 73 .inner 73 74 .repository_cache ··· 104 105 } 105 106 } 106 107 107 - fn reopen(&self, repo: &RepoId) -> Result<Repository, XrpcError> { 108 + fn reopen(&self, repo: &RepoPath) -> Result<Repository, XrpcError> { 108 109 { 109 110 let mut cache = self 110 111 .inner
+5 -7
crates/knot/src/public/git.rs
··· 1 1 use crate::{ 2 - RepoId, 2 + RepoPath, 3 3 model::{Knot, RepositoryManager as _}, 4 4 public::git::{helpers::SetOptionEnv as _, protocol::GitProtocol}, 5 - repoid::RepoPath, 6 5 }; 7 6 use axum::{ 8 7 Router, ··· 127 128 Query(InfoRefsQuery { service }): Query<InfoRefsQuery>, 128 129 protocol: Option<GitProtocol>, 129 130 ) -> Result<Response> { 130 - let repo = repo.into(); 131 131 match service { 132 132 GitService::GitUploadPack => advertise_upload_pack(knot, repo, protocol).await, 133 133 GitService::GitReceivePack => advertise_receive_pack(knot, repo, protocol).await, ··· 137 139 #[tracing::instrument] 138 140 pub async fn advertise_upload_pack( 139 141 knot: Knot, 140 - repo: RepoId, 142 + repo: RepoPath, 141 143 protocol: Option<GitProtocol>, 142 144 ) -> Result<Response> { 143 145 let repo = knot.open(&repo).map_err(|_| Error::not_found())?; ··· 182 184 #[tracing::instrument] 183 185 pub async fn advertise_receive_pack( 184 186 knot: Knot, 185 - repo: RepoId, 187 + repo: RepoPath, 186 188 protocol: Option<GitProtocol>, 187 189 ) -> Result<Response> { 188 190 let repo = knot.reopen(&repo).map_err(|_| Error::not_found())?; ··· 229 231 protocol: Option<GitProtocol>, 230 232 body: Body, 231 233 ) -> Result<Response> { 232 - let repo = knot.open(&repo.into()).map_err(|_| Error::not_found())?; 234 + let repo = knot.open(&repo).map_err(|_| Error::not_found())?; 233 235 234 236 // The request body for git-upload-pack should be quite small, so just 235 237 // buffer it. ··· 278 280 protocol: Option<GitProtocol>, 279 281 body: Body, 280 282 ) -> Result<impl IntoResponse> { 281 - let repo = knot.reopen(&repo.into()).map_err(|_| Error::not_found())?; 283 + let repo = knot.reopen(&repo).map_err(|_| Error::not_found())?; 282 284 check_push_status(&repo)?; 283 285 284 286 let mut child = Command::new(GIT_COMMAND)
+71 -44
crates/knot/src/repoid.rs
··· 3 3 de::{Error, Visitor}, 4 4 }; 5 5 6 - pub trait RepoParts { 7 - fn owner(&self) -> &str; 8 - fn name(&self) -> &str; 9 - } 10 - 11 6 #[derive(Clone, Debug, PartialEq, Eq, Hash)] 12 - pub struct RepoId { 13 - /// DID of the repository owner 14 - pub owner: Box<str>, 15 - 16 - /// Repository name 17 - pub name: Box<str>, 18 - } 19 - 20 - macro_rules! impl_repo_parts { 21 - ($typ:ty) => { 22 - impl RepoParts for $typ { 23 - fn owner(&self) -> &str { 24 - &self.owner 25 - } 26 - 27 - fn name(&self) -> &str { 28 - &self.name 29 - } 30 - } 31 - }; 32 - } 33 - 34 - impl_repo_parts!(RepoId); 35 - impl_repo_parts!(RepoPath); 36 - 37 - #[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)] 38 7 pub struct RepoPath { 39 8 /// DID of the repository owner 40 9 pub owner: Box<str>, ··· 12 43 pub name: Box<str>, 13 44 } 14 45 15 - impl From<RepoId> for RepoPath { 16 - fn from(RepoId { owner, name }: RepoId) -> Self { 17 - Self { owner, name } 46 + impl RepoPath { 47 + pub const fn owner(&self) -> &str { 48 + &self.owner 49 + } 50 + 51 + pub const fn name(&self) -> &str { 52 + &self.name 18 53 } 19 54 } 20 55 21 - impl From<RepoPath> for RepoId { 22 - fn from(RepoPath { owner, name }: RepoPath) -> Self { 23 - Self { owner, name } 24 - } 25 - } 26 - 27 - impl<'de> Deserialize<'de> for RepoId { 56 + impl<'de> Deserialize<'de> for RepoPath { 28 57 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 29 58 where 30 59 D: Deserializer<'de>, ··· 30 63 struct RepoIdVisitor; 31 64 32 65 impl<'de> Visitor<'de> for RepoIdVisitor { 33 - type Value = RepoId; 66 + type Value = RepoPath; 34 67 35 68 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 36 69 formatter.write_str("repository identifier in the form '{owner}/{name}'") ··· 46 79 )); 47 80 }; 48 81 49 - // @TODO validate owner and name! 82 + validate(owner).map_err(Error::custom)?; 83 + validate(name).map_err(Error::custom)?; 50 84 51 85 Ok(Self::Value { 52 86 owner: owner.into(), 53 87 name: name.into(), 54 88 }) 55 89 } 90 + 91 + fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> 92 + where 93 + A: serde::de::SeqAccess<'de>, 94 + { 95 + let owner: Box<str> = seq 96 + .next_element()? 97 + .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; 98 + let name: Box<str> = seq 99 + .next_element()? 100 + .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; 101 + 102 + validate(&owner).map_err(Error::custom)?; 103 + validate(&name).map_err(Error::custom)?; 104 + 105 + Ok(Self::Value { owner, name }) 106 + } 56 107 } 57 108 58 - deserializer.deserialize_str(RepoIdVisitor) 109 + deserializer.deserialize_seq(RepoIdVisitor) 59 110 } 111 + } 112 + 113 + fn validate(name: &str) -> Result<(), ValidationError> { 114 + static FORBIDDEN_SUBSTRINGS: &[&str] = &[".."]; 115 + static FORBIDDEN_PREFIXES: &[&str] = &[".", "-"]; 116 + static ACCEPTED_SYMBOLS: &[char] = &['.', '-', '_', ':']; 117 + 118 + if name.is_empty() { 119 + return Err(ValidationError::Empty); 120 + } 121 + 122 + for value in name.chars() { 123 + if !(value.is_ascii_alphanumeric() || ACCEPTED_SYMBOLS.contains(&value)) { 124 + return Err(ValidationError::Character { value }); 125 + } 126 + } 127 + 128 + for substring in FORBIDDEN_SUBSTRINGS { 129 + if name.contains(substring) { 130 + return Err(ValidationError::Substring { substring }); 131 + } 132 + } 133 + 134 + for prefix in FORBIDDEN_PREFIXES { 135 + if name.starts_with(prefix) { 136 + return Err(ValidationError::Prefix { prefix }); 137 + } 138 + } 139 + 140 + Ok(()) 141 + } 142 + 143 + #[derive(Debug, thiserror::Error)] 144 + enum ValidationError { 145 + #[error("repository name cannot be empty")] 146 + Empty, 147 + #[error("invalid character in repository name: '{value}'")] 148 + Character { value: char }, 149 + #[error("invalid sub-string in repository name: '{substring}'")] 150 + Substring { substring: &'static str }, 151 + #[error("invalid prefix for respository name: '{prefix}'")] 152 + Prefix { prefix: &'static str }, 60 153 }
+8 -8
crates/knot/src/types.rs
··· 31 31 } 32 32 33 33 pub mod repo { 34 - use crate::{ObjectId, RepoId, objectid::Array}; 34 + use crate::{ObjectId, RepoPath, objectid::Array}; 35 35 use serde::{Deserialize, Serialize}; 36 36 use std::{borrow::Cow, collections::HashMap, path::PathBuf}; 37 37 use time::OffsetDateTime; 38 38 39 39 #[derive(Debug, Deserialize)] 40 40 pub struct GetDefaultBranchParams { 41 - pub repo: RepoId, 41 + pub repo: RepoPath, 42 42 } 43 43 44 44 /// Response type for the `sh.tangled.repo.getDefaultBranch` query. ··· 112 112 113 113 #[derive(Debug, Deserialize)] 114 114 pub struct BranchesParams { 115 - pub repo: RepoId, 115 + pub repo: RepoPath, 116 116 #[serde(default = "branches_limit_default")] 117 117 pub limit: u16, 118 118 #[serde(default)] ··· 141 141 142 142 #[derive(Debug, Deserialize)] 143 143 pub struct LogParams { 144 - pub repo: RepoId, 144 + pub repo: RepoPath, 145 145 #[serde(rename = "ref")] 146 146 pub rev: Option<String>, 147 147 // @NOTE The lexicon states `cursor` is a string containing a pagination ··· 159 159 160 160 #[derive(Debug, Deserialize)] 161 161 pub struct TagParams { 162 - pub repo: RepoId, 162 + pub repo: RepoPath, 163 163 #[serde(default)] 164 164 pub cursor: usize, 165 165 #[serde(default = "tag_limit_default")] ··· 207 207 208 208 #[derive(Debug, Deserialize)] 209 209 pub struct TreeParams { 210 - pub repo: RepoId, 210 + pub repo: RepoPath, 211 211 #[serde(rename = "ref")] 212 212 pub rev: Option<String>, 213 213 pub path: Option<PathBuf>, ··· 235 235 236 236 #[derive(Debug, Deserialize)] 237 237 pub struct BlobParams { 238 - pub repo: RepoId, 238 + pub repo: RepoPath, 239 239 #[serde(rename = "ref")] 240 240 pub rev: Option<String>, 241 241 pub path: PathBuf, ··· 327 327 328 328 #[derive(Debug, Deserialize)] 329 329 pub struct DiffParams { 330 - pub repo: RepoId, 330 + pub repo: RepoPath, 331 331 #[serde(rename = "ref")] 332 332 pub rev: String, 333 333 }