Skip to main content

guts_compat/
store.rs

1//! Storage for compatibility layer data.
2
3use parking_lot::RwLock;
4use std::collections::HashMap;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::Arc;
7
8use crate::error::{CompatError, Result};
9use crate::rate_limit::RateLimiter;
10use crate::release::{AssetId, Release, ReleaseAsset, ReleaseId};
11use crate::ssh_key::{SshKey, SshKeyId};
12use crate::token::{PersonalAccessToken, TokenId, TokenScope, TokenValue};
13use crate::user::{User, UserId};
14
15/// Compatibility layer data store.
16#[derive(Debug, Clone)]
17pub struct CompatStore {
18    /// User storage.
19    pub users: UserStore,
20    /// Token storage.
21    pub tokens: TokenStore,
22    /// SSH key storage.
23    pub ssh_keys: SshKeyStore,
24    /// Release storage.
25    pub releases: ReleaseStore,
26    /// Rate limiter.
27    pub rate_limiter: RateLimiter,
28}
29
30impl Default for CompatStore {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36impl CompatStore {
37    /// Create a new compatibility store.
38    pub fn new() -> Self {
39        Self {
40            users: UserStore::new(),
41            tokens: TokenStore::new(),
42            ssh_keys: SshKeyStore::new(),
43            releases: ReleaseStore::new(),
44            rate_limiter: RateLimiter::new(),
45        }
46    }
47
48    /// Get statistics about stored data.
49    pub fn stats(&self) -> CompatStats {
50        CompatStats {
51            users: self.users.count(),
52            tokens: self.tokens.count(),
53            ssh_keys: self.ssh_keys.count(),
54            releases: self.releases.count(),
55        }
56    }
57}
58
59/// User storage.
60#[derive(Debug, Clone)]
61pub struct UserStore {
62    /// Users by ID.
63    users: Arc<RwLock<HashMap<UserId, User>>>,
64    /// Username to ID index.
65    username_index: Arc<RwLock<HashMap<String, UserId>>>,
66    /// Public key to ID index.
67    pubkey_index: Arc<RwLock<HashMap<String, UserId>>>,
68    /// Next user ID.
69    next_id: Arc<AtomicU64>,
70}
71
72impl Default for UserStore {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78impl UserStore {
79    /// Create a new user store.
80    pub fn new() -> Self {
81        Self {
82            users: Arc::new(RwLock::new(HashMap::new())),
83            username_index: Arc::new(RwLock::new(HashMap::new())),
84            pubkey_index: Arc::new(RwLock::new(HashMap::new())),
85            next_id: Arc::new(AtomicU64::new(1)),
86        }
87    }
88
89    /// Create a new user.
90    pub fn create(&self, username: String, public_key: String) -> Result<User> {
91        // Validate username
92        User::validate_username(&username).map_err(CompatError::InvalidUsername)?;
93
94        let mut users = self.users.write();
95        let mut username_index = self.username_index.write();
96        let mut pubkey_index = self.pubkey_index.write();
97
98        // Check for duplicate username
99        if username_index.contains_key(&username) {
100            return Err(CompatError::UsernameExists(username));
101        }
102
103        // Check for duplicate public key
104        if pubkey_index.contains_key(&public_key) {
105            return Err(CompatError::UsernameExists(
106                "public key already registered".to_string(),
107            ));
108        }
109
110        let id = self.next_id.fetch_add(1, Ordering::SeqCst);
111        let user = User::new(id, username.clone(), public_key.clone());
112
113        username_index.insert(username, id);
114        pubkey_index.insert(public_key, id);
115        users.insert(id, user.clone());
116
117        Ok(user)
118    }
119
120    /// Get a user by ID.
121    pub fn get(&self, id: UserId) -> Option<User> {
122        self.users.read().get(&id).cloned()
123    }
124
125    /// Get a user by username.
126    pub fn get_by_username(&self, username: &str) -> Option<User> {
127        let username_index = self.username_index.read();
128        let id = username_index.get(username)?;
129        self.users.read().get(id).cloned()
130    }
131
132    /// Get a user by public key.
133    pub fn get_by_public_key(&self, public_key: &str) -> Option<User> {
134        let pubkey_index = self.pubkey_index.read();
135        let id = pubkey_index.get(public_key)?;
136        self.users.read().get(id).cloned()
137    }
138
139    /// Update a user.
140    pub fn update(&self, user: User) -> Result<User> {
141        let mut users = self.users.write();
142        if !users.contains_key(&user.id) {
143            return Err(CompatError::UserNotFound(user.id.to_string()));
144        }
145        users.insert(user.id, user.clone());
146        Ok(user)
147    }
148
149    /// List all users.
150    pub fn list(&self) -> Vec<User> {
151        self.users.read().values().cloned().collect()
152    }
153
154    /// Count users.
155    pub fn count(&self) -> usize {
156        self.users.read().len()
157    }
158}
159
160/// Token storage.
161#[derive(Debug, Clone)]
162pub struct TokenStore {
163    /// Tokens by ID.
164    tokens: Arc<RwLock<HashMap<TokenId, PersonalAccessToken>>>,
165    /// Token prefix to ID index.
166    prefix_index: Arc<RwLock<HashMap<String, TokenId>>>,
167    /// Next token ID.
168    next_id: Arc<AtomicU64>,
169}
170
171impl Default for TokenStore {
172    fn default() -> Self {
173        Self::new()
174    }
175}
176
177impl TokenStore {
178    /// Create a new token store.
179    pub fn new() -> Self {
180        Self {
181            tokens: Arc::new(RwLock::new(HashMap::new())),
182            prefix_index: Arc::new(RwLock::new(HashMap::new())),
183            next_id: Arc::new(AtomicU64::new(1)),
184        }
185    }
186
187    /// Create a new token.
188    ///
189    /// Returns the token struct and the plaintext token (only shown once).
190    pub fn create(
191        &self,
192        user_id: UserId,
193        name: String,
194        scopes: Vec<TokenScope>,
195        expires_at: Option<u64>,
196    ) -> Result<(PersonalAccessToken, String)> {
197        let id = self.next_id.fetch_add(1, Ordering::SeqCst);
198        let (token, plaintext) =
199            PersonalAccessToken::generate(id, user_id, name, scopes, expires_at)?;
200
201        let mut tokens = self.tokens.write();
202        let mut prefix_index = self.prefix_index.write();
203
204        prefix_index.insert(token.token_prefix.clone(), id);
205        tokens.insert(id, token.clone());
206
207        Ok((token, plaintext))
208    }
209
210    /// Get a token by ID.
211    pub fn get(&self, id: TokenId) -> Option<PersonalAccessToken> {
212        self.tokens.read().get(&id).cloned()
213    }
214
215    /// Get a token by prefix.
216    pub fn get_by_prefix(&self, prefix: &str) -> Option<PersonalAccessToken> {
217        let prefix_index = self.prefix_index.read();
218        let id = prefix_index.get(prefix)?;
219        self.tokens.read().get(id).cloned()
220    }
221
222    /// Verify a token and return the user ID if valid.
223    pub fn verify(&self, token_string: &str) -> Result<(UserId, Vec<TokenScope>)> {
224        let token_value = TokenValue::parse(token_string)?;
225
226        let prefix_index = self.prefix_index.read();
227        let id = prefix_index
228            .get(&token_value.prefix)
229            .ok_or(CompatError::TokenNotFound)?;
230
231        let mut tokens = self.tokens.write();
232        let token = tokens.get_mut(id).ok_or(CompatError::TokenNotFound)?;
233
234        // Verify the secret
235        token.verify(&token_value.secret)?;
236
237        // Check expiration
238        if token.is_expired() {
239            return Err(CompatError::TokenExpired);
240        }
241
242        // Update last used
243        token.touch();
244
245        Ok((token.user_id, token.scopes.clone()))
246    }
247
248    /// Revoke (delete) a token.
249    pub fn revoke(&self, id: TokenId) -> Result<()> {
250        let mut tokens = self.tokens.write();
251        let mut prefix_index = self.prefix_index.write();
252
253        let token = tokens.remove(&id).ok_or(CompatError::TokenNotFound)?;
254        prefix_index.remove(&token.token_prefix);
255
256        Ok(())
257    }
258
259    /// List tokens for a user (without secrets).
260    pub fn list_for_user(&self, user_id: UserId) -> Vec<PersonalAccessToken> {
261        self.tokens
262            .read()
263            .values()
264            .filter(|t| t.user_id == user_id)
265            .cloned()
266            .collect()
267    }
268
269    /// Count tokens.
270    pub fn count(&self) -> usize {
271        self.tokens.read().len()
272    }
273}
274
275/// SSH key storage.
276#[derive(Debug, Clone)]
277pub struct SshKeyStore {
278    /// Keys by ID.
279    keys: Arc<RwLock<HashMap<SshKeyId, SshKey>>>,
280    /// Fingerprint to ID index.
281    fingerprint_index: Arc<RwLock<HashMap<String, SshKeyId>>>,
282    /// Next key ID.
283    next_id: Arc<AtomicU64>,
284}
285
286impl Default for SshKeyStore {
287    fn default() -> Self {
288        Self::new()
289    }
290}
291
292impl SshKeyStore {
293    /// Create a new SSH key store.
294    pub fn new() -> Self {
295        Self {
296            keys: Arc::new(RwLock::new(HashMap::new())),
297            fingerprint_index: Arc::new(RwLock::new(HashMap::new())),
298            next_id: Arc::new(AtomicU64::new(1)),
299        }
300    }
301
302    /// Add an SSH key.
303    pub fn add(&self, user_id: UserId, title: String, public_key: String) -> Result<SshKey> {
304        let id = self.next_id.fetch_add(1, Ordering::SeqCst);
305        let key = SshKey::new(id, user_id, title, public_key)?;
306
307        let mut keys = self.keys.write();
308        let mut fingerprint_index = self.fingerprint_index.write();
309
310        // Check for duplicate fingerprint
311        if fingerprint_index.contains_key(&key.fingerprint) {
312            return Err(CompatError::SshKeyExists(key.fingerprint));
313        }
314
315        fingerprint_index.insert(key.fingerprint.clone(), id);
316        keys.insert(id, key.clone());
317
318        Ok(key)
319    }
320
321    /// Get an SSH key by ID.
322    pub fn get(&self, id: SshKeyId) -> Option<SshKey> {
323        self.keys.read().get(&id).cloned()
324    }
325
326    /// Get an SSH key by fingerprint.
327    pub fn get_by_fingerprint(&self, fingerprint: &str) -> Option<SshKey> {
328        let fingerprint_index = self.fingerprint_index.read();
329        let id = fingerprint_index.get(fingerprint)?;
330        self.keys.read().get(id).cloned()
331    }
332
333    /// Remove an SSH key.
334    pub fn remove(&self, id: SshKeyId) -> Result<SshKey> {
335        let mut keys = self.keys.write();
336        let mut fingerprint_index = self.fingerprint_index.write();
337
338        let key = keys.remove(&id).ok_or(CompatError::SshKeyNotFound)?;
339        fingerprint_index.remove(&key.fingerprint);
340
341        Ok(key)
342    }
343
344    /// List SSH keys for a user.
345    pub fn list_for_user(&self, user_id: UserId) -> Vec<SshKey> {
346        self.keys
347            .read()
348            .values()
349            .filter(|k| k.user_id == user_id)
350            .cloned()
351            .collect()
352    }
353
354    /// Count SSH keys.
355    pub fn count(&self) -> usize {
356        self.keys.read().len()
357    }
358}
359
360/// Release storage.
361#[derive(Debug, Clone)]
362pub struct ReleaseStore {
363    /// Releases by ID.
364    releases: Arc<RwLock<HashMap<ReleaseId, Release>>>,
365    /// (repo_key, tag_name) to ID index.
366    tag_index: Arc<RwLock<HashMap<(String, String), ReleaseId>>>,
367    /// Asset content storage (hash -> bytes).
368    asset_content: Arc<RwLock<HashMap<String, Vec<u8>>>>,
369    /// Next release ID.
370    next_release_id: Arc<AtomicU64>,
371    /// Next asset ID.
372    next_asset_id: Arc<AtomicU64>,
373}
374
375impl Default for ReleaseStore {
376    fn default() -> Self {
377        Self::new()
378    }
379}
380
381impl ReleaseStore {
382    /// Create a new release store.
383    pub fn new() -> Self {
384        Self {
385            releases: Arc::new(RwLock::new(HashMap::new())),
386            tag_index: Arc::new(RwLock::new(HashMap::new())),
387            asset_content: Arc::new(RwLock::new(HashMap::new())),
388            next_release_id: Arc::new(AtomicU64::new(1)),
389            next_asset_id: Arc::new(AtomicU64::new(1)),
390        }
391    }
392
393    /// Create a new release.
394    pub fn create(
395        &self,
396        repo_key: String,
397        tag_name: String,
398        target_commitish: String,
399        author: String,
400    ) -> Result<Release> {
401        let mut releases = self.releases.write();
402        let mut tag_index = self.tag_index.write();
403
404        // Check for duplicate tag
405        let key = (repo_key.clone(), tag_name.clone());
406        if tag_index.contains_key(&key) {
407            return Err(CompatError::ReleaseExists(tag_name));
408        }
409
410        let id = self.next_release_id.fetch_add(1, Ordering::SeqCst);
411        let release = Release::new(id, repo_key, tag_name, target_commitish, author);
412
413        tag_index.insert(key, id);
414        releases.insert(id, release.clone());
415
416        Ok(release)
417    }
418
419    /// Get a release by ID.
420    pub fn get(&self, id: ReleaseId) -> Option<Release> {
421        self.releases.read().get(&id).cloned()
422    }
423
424    /// Get a release by tag.
425    pub fn get_by_tag(&self, repo_key: &str, tag_name: &str) -> Option<Release> {
426        let tag_index = self.tag_index.read();
427        let id = tag_index.get(&(repo_key.to_string(), tag_name.to_string()))?;
428        self.releases.read().get(id).cloned()
429    }
430
431    /// Get the latest release for a repository.
432    pub fn get_latest(&self, repo_key: &str) -> Option<Release> {
433        self.releases
434            .read()
435            .values()
436            .filter(|r| r.repo_key == repo_key && r.is_publishable())
437            .max_by_key(|r| r.published_at)
438            .cloned()
439    }
440
441    /// Update a release.
442    pub fn update(&self, release: Release) -> Result<Release> {
443        let mut releases = self.releases.write();
444        if !releases.contains_key(&release.id) {
445            return Err(CompatError::ReleaseNotFound(release.id.to_string()));
446        }
447        releases.insert(release.id, release.clone());
448        Ok(release)
449    }
450
451    /// Delete a release.
452    pub fn delete(&self, id: ReleaseId) -> Result<Release> {
453        let mut releases = self.releases.write();
454        let mut tag_index = self.tag_index.write();
455
456        let release = releases
457            .remove(&id)
458            .ok_or_else(|| CompatError::ReleaseNotFound(id.to_string()))?;
459        tag_index.remove(&(release.repo_key.clone(), release.tag_name.clone()));
460
461        Ok(release)
462    }
463
464    /// List releases for a repository.
465    pub fn list(&self, repo_key: &str) -> Vec<Release> {
466        let mut releases: Vec<_> = self
467            .releases
468            .read()
469            .values()
470            .filter(|r| r.repo_key == repo_key)
471            .cloned()
472            .collect();
473        releases.sort_by(|a, b| b.created_at.cmp(&a.created_at));
474        releases
475    }
476
477    /// Add an asset to a release.
478    pub fn add_asset(
479        &self,
480        release_id: ReleaseId,
481        name: String,
482        content_type: String,
483        content: Vec<u8>,
484        uploader: String,
485    ) -> Result<ReleaseAsset> {
486        let mut releases = self.releases.write();
487        let mut asset_content = self.asset_content.write();
488
489        let release = releases
490            .get_mut(&release_id)
491            .ok_or_else(|| CompatError::ReleaseNotFound(release_id.to_string()))?;
492
493        // Check for duplicate asset name
494        if release.assets.iter().any(|a| a.name == name) {
495            return Err(CompatError::AssetExists(name));
496        }
497
498        // Calculate hash
499        use sha2::{Digest, Sha256};
500        let hash = hex::encode(Sha256::digest(&content));
501
502        let id = self.next_asset_id.fetch_add(1, Ordering::SeqCst);
503        let asset = ReleaseAsset::new(
504            id,
505            release_id,
506            name,
507            content_type,
508            content.len() as u64,
509            hash.clone(),
510            uploader,
511        );
512
513        // Store content
514        asset_content.insert(hash, content);
515
516        // Add to release
517        release.add_asset(asset.clone());
518
519        Ok(asset)
520    }
521
522    /// Get asset content.
523    pub fn get_asset_content(&self, content_hash: &str) -> Option<Vec<u8>> {
524        self.asset_content.read().get(content_hash).cloned()
525    }
526
527    /// Delete an asset.
528    pub fn delete_asset(&self, release_id: ReleaseId, asset_id: AssetId) -> Result<ReleaseAsset> {
529        let mut releases = self.releases.write();
530        let mut asset_content = self.asset_content.write();
531
532        let release = releases
533            .get_mut(&release_id)
534            .ok_or_else(|| CompatError::ReleaseNotFound(release_id.to_string()))?;
535
536        let asset = release
537            .remove_asset(asset_id)
538            .ok_or_else(|| CompatError::AssetNotFound(asset_id.to_string()))?;
539
540        // Remove content
541        asset_content.remove(&asset.content_hash);
542
543        Ok(asset)
544    }
545
546    /// Count releases.
547    pub fn count(&self) -> usize {
548        self.releases.read().len()
549    }
550}
551
552/// Statistics about stored data.
553#[derive(Debug, Clone, Serialize)]
554pub struct CompatStats {
555    /// Number of users.
556    pub users: usize,
557    /// Number of tokens.
558    pub tokens: usize,
559    /// Number of SSH keys.
560    pub ssh_keys: usize,
561    /// Number of releases.
562    pub releases: usize,
563}
564
565use serde::Serialize;
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570
571    #[test]
572    fn test_user_store() {
573        let store = UserStore::new();
574
575        // Create a user
576        let user = store
577            .create("alice".to_string(), "pubkey123".to_string())
578            .unwrap();
579        assert_eq!(user.username, "alice");
580
581        // Get by ID
582        let found = store.get(user.id).unwrap();
583        assert_eq!(found.username, "alice");
584
585        // Get by username
586        let found = store.get_by_username("alice").unwrap();
587        assert_eq!(found.id, user.id);
588
589        // Duplicate username should fail
590        let result = store.create("alice".to_string(), "pubkey456".to_string());
591        assert!(result.is_err());
592    }
593
594    #[test]
595    fn test_token_store() {
596        let store = TokenStore::new();
597
598        // Create a token
599        let (token, plaintext) = store
600            .create(
601                1,
602                "test token".to_string(),
603                vec![TokenScope::RepoRead],
604                None,
605            )
606            .unwrap();
607
608        assert!(plaintext.starts_with("guts_"));
609
610        // Verify the token
611        let (user_id, scopes) = store.verify(&plaintext).unwrap();
612        assert_eq!(user_id, 1);
613        assert!(scopes.contains(&TokenScope::RepoRead));
614
615        // Revoke the token
616        store.revoke(token.id).unwrap();
617
618        // Verification should fail
619        let result = store.verify(&plaintext);
620        assert!(result.is_err());
621    }
622
623    #[test]
624    fn test_ssh_key_store() {
625        let store = SshKeyStore::new();
626
627        // Add a key
628        let key = store
629            .add(
630                1,
631                "My Key".to_string(),
632                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl test@example.com".to_string(),
633            )
634            .unwrap();
635
636        // Get by ID
637        let found = store.get(key.id).unwrap();
638        assert_eq!(found.title, "My Key");
639
640        // List for user
641        let keys = store.list_for_user(1);
642        assert_eq!(keys.len(), 1);
643
644        // Remove
645        store.remove(key.id).unwrap();
646        assert!(store.get(key.id).is_none());
647    }
648
649    #[test]
650    fn test_release_store() {
651        let store = ReleaseStore::new();
652
653        // Create a release
654        let release = store
655            .create(
656                "alice/repo".to_string(),
657                "v1.0.0".to_string(),
658                "main".to_string(),
659                "alice".to_string(),
660            )
661            .unwrap();
662
663        assert_eq!(release.tag_name, "v1.0.0");
664
665        // Get by ID
666        let found = store.get(release.id).unwrap();
667        assert_eq!(found.tag_name, "v1.0.0");
668
669        // Get by tag
670        let found = store.get_by_tag("alice/repo", "v1.0.0").unwrap();
671        assert_eq!(found.id, release.id);
672
673        // Get latest
674        let latest = store.get_latest("alice/repo").unwrap();
675        assert_eq!(latest.id, release.id);
676
677        // Add asset
678        let asset = store
679            .add_asset(
680                release.id,
681                "app.tar.gz".to_string(),
682                "application/gzip".to_string(),
683                b"test content".to_vec(),
684                "alice".to_string(),
685            )
686            .unwrap();
687
688        assert_eq!(asset.name, "app.tar.gz");
689
690        // Get asset content
691        let content = store.get_asset_content(&asset.content_hash).unwrap();
692        assert_eq!(content, b"test content");
693
694        // Delete release
695        store.delete(release.id).unwrap();
696        assert!(store.get(release.id).is_none());
697    }
698}