steid

@jamesgill /

1//! Service ports the application layer needs.
2//!
3//! Repository ports live in `domain::repository`; these are the non-persistence
4//! collaborators. Adapters live in `infrastructure`.
5
6use std::path::PathBuf;
7
8use crate::domain::{OrgName, PasswordHash, RepoName};
9
10/// Hashes and verifies passwords.
11///
12/// A port rather than a direct Argon2 call so tests can substitute a fast stub —
13/// Argon2 is deliberately slow, and a use case suite that hashes for real takes
14/// seconds per test.
15pub trait PasswordHasher: Send + Sync {
16 /// Hashes a plaintext password.
17 fn hash(&self, plaintext: &str) -> Result<PasswordHash, PasswordError>;
18
19 /// Checks a plaintext password against a stored hash.
20 ///
21 /// Returns `Ok(false)` for a wrong password and `Err` only when the hash itself
22 /// cannot be parsed — a corrupt stored hash is a different problem from a failed
23 /// login, and collapsing them hides real faults.
24 fn verify(&self, plaintext: &str, hash: &PasswordHash) -> Result<bool, PasswordError>;
25}
26
27/// A password could not be hashed or a stored hash could not be parsed.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct PasswordError(String);
30
31impl PasswordError {
32 pub fn new(message: impl Into<String>) -> Self {
33 Self(message.into())
34 }
35}
36
37impl std::fmt::Display for PasswordError {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 write!(f, "password hashing failed: {}", self.0)
40 }
41}
42
43impl std::error::Error for PasswordError {}
44
45/// Where bare git repositories live on disk.
46///
47/// Laid out as `{data_dir}/{handle}/{name}.git`. Keyed by handle rather than
48/// [`OrgId`](crate::domain::OrgId) so the data directory is legible to anyone who has
49/// to debug it; the cost is that renaming a handle becomes a directory move rather
50/// than a row update.
51///
52/// Deliberately narrow. Serving the git protocol and browsing a tree are separate
53/// concerns with separate shapes — streaming and querying — and get their own ports as
54/// their use cases arrive, rather than accreting here. See
55/// [0006](../../plans/decisions/0006-git-binary-behind-narrow-ports.md).
56pub trait GitStorage: Send + Sync {
57 /// Creates an empty bare repository.
58 ///
59 /// Empty means empty: no initial commit, no branch, matching what GitHub does for
60 /// a repository created without a README.
61 ///
62 /// Fails with [`GitStorageError::AlreadyExists`] rather than adopting whatever is
63 /// already there.
64 fn init_bare(
65 &self,
66 handle: &OrgName,
67 name: &RepoName,
68 ) -> impl Future<Output = Result<(), GitStorageError>> + Send;
69
70 /// Removes a bare repository, succeeding if there was nothing to remove.
71 ///
72 /// This exists to compensate a failed record insert — a repository row and its
73 /// directory cannot share a transaction — not as a user-facing delete. Deleting a
74 /// repository properly is a separate use case with its own authorization.
75 fn remove(
76 &self,
77 handle: &OrgName,
78 name: &RepoName,
79 ) -> impl Future<Output = Result<(), GitStorageError>> + Send;
80
81 /// Where a repository lives.
82 ///
83 /// Pure, and says nothing about whether anything exists there. Milestone 4 hands
84 /// this to `git http-backend`.
85 fn repo_path(&self, handle: &OrgName, name: &RepoName) -> PathBuf;
86}
87
88/// A repository could not be created or removed on disk.
89#[derive(Debug)]
90pub enum GitStorageError {
91 /// Something already occupies the repository's path.
92 ///
93 /// Kept distinct from a general failure because it is the one case a use case can
94 /// explain to a user, and because with no matching record it means an orphaned
95 /// directory left by a crashed create.
96 AlreadyExists,
97 /// The filesystem or the `git` binary failed.
98 Backend(Box<dyn std::error::Error + Send + Sync>),
99}
100
101impl GitStorageError {
102 pub fn backend(error: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
103 Self::Backend(error.into())
104 }
105}
106
107impl std::fmt::Display for GitStorageError {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 Self::AlreadyExists => f.write_str("a repository already exists at that path"),
111 Self::Backend(error) => write!(f, "git storage failure: {error}"),
112 }
113 }
114}
115
116impl std::error::Error for GitStorageError {
117 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
118 match self {
119 Self::AlreadyExists => None,
120 Self::Backend(error) => Some(&**error),
121 }
122 }
123}