Skip to main content

shardline_server/
protocol_support.rs

1use shardline_server_core::protocol_support as core_ps;
2
3use crate::ServerError;
4
5impl core_ps::ProtocolValidation for ServerError {
6    fn invalid_digest() -> Self {
7        Self::InvalidDigest
8    }
9    fn invalid_content_hash() -> Self {
10        Self::InvalidContentHash
11    }
12    fn invalid_repository_name() -> Self {
13        Self::InvalidRepositoryName
14    }
15    fn not_found() -> Self {
16        Self::NotFound
17    }
18    fn invalid_manifest_reference() -> Self {
19        Self::InvalidManifestReference
20    }
21    fn invalid_upload_session() -> Self {
22        Self::InvalidUploadSession
23    }
24}
25
26pub(crate) fn parse_sha256_digest(value: &str) -> Result<String, ServerError> {
27    core_ps::parse_sha256_digest(value)
28}
29
30pub(crate) fn scope_namespace(
31    repository_scope: Option<&shardline_protocol::RepositoryScope>,
32) -> String {
33    core_ps::scope_namespace(repository_scope)
34}
35
36pub fn shared_sha256_object_key(
37    digest_hex: &str,
38) -> Result<shardline_storage::ObjectKey, ServerError> {
39    core_ps::shared_sha256_object_key(digest_hex)
40}
41
42pub(crate) fn validate_oci_repository_name(value: &str) -> Result<(), ServerError> {
43    core_ps::validate_oci_repository_name(value)
44}
45
46pub(crate) fn validate_oci_repository_scope(
47    value: &str,
48    repository_scope: Option<&shardline_protocol::RepositoryScope>,
49) -> Result<(), ServerError> {
50    core_ps::validate_oci_repository_scope(value, repository_scope)
51}
52
53pub(crate) fn validate_oci_tag(value: &str) -> Result<(), ServerError> {
54    core_ps::validate_oci_tag(value)
55}
56
57pub(crate) fn validate_upload_session_id(value: &str) -> Result<(), ServerError> {
58    core_ps::validate_upload_session_id(value)
59}
60
61#[cfg(test)]
62mod tests {
63    use super::{
64        parse_sha256_digest, shared_sha256_object_key, validate_oci_repository_name,
65        validate_oci_repository_scope, validate_oci_tag, validate_upload_session_id,
66    };
67    use crate::ServerError;
68    use shardline_protocol::{RepositoryProvider, RepositoryScope};
69
70    #[test]
71    fn sha256_digest_parser_requires_prefixed_lowercase_hex() {
72        let digest = parse_sha256_digest(
73            "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
74        );
75        assert!(digest.is_ok());
76        assert_eq!(
77            digest.unwrap_or_default(),
78            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
79        );
80        assert!(matches!(
81            parse_sha256_digest("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
82            Err(ServerError::InvalidDigest)
83        ));
84        assert!(matches!(
85            parse_sha256_digest(
86                "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdeg"
87            ),
88            Err(ServerError::InvalidDigest)
89        ));
90    }
91
92    #[test]
93    fn oci_repository_validator_rejects_traversal_and_uppercase() {
94        assert!(validate_oci_repository_name("team/assets").is_ok());
95        assert!(matches!(
96            validate_oci_repository_name("../assets"),
97            Err(ServerError::InvalidRepositoryName)
98        ));
99        assert!(matches!(
100            validate_oci_repository_name("Team/assets"),
101            Err(ServerError::InvalidRepositoryName)
102        ));
103        assert!(matches!(
104            validate_oci_repository_name("team//assets"),
105            Err(ServerError::InvalidRepositoryName)
106        ));
107    }
108
109    #[test]
110    fn oci_tag_validator_enforces_allowed_characters() {
111        assert!(validate_oci_tag("v1").is_ok());
112        assert!(validate_oci_tag("_debug.2026-04-23").is_ok());
113        assert!(matches!(
114            validate_oci_tag("bad/tag"),
115            Err(ServerError::InvalidManifestReference)
116        ));
117        assert!(matches!(
118            validate_oci_tag("-bad"),
119            Err(ServerError::InvalidManifestReference)
120        ));
121    }
122
123    #[test]
124    fn upload_session_validator_accepts_hex_and_hyphen_only() {
125        assert!(validate_upload_session_id("0000000000000001").is_ok());
126        assert!(validate_upload_session_id("dead-beef").is_ok());
127        assert!(matches!(
128            validate_upload_session_id("session_1"),
129            Err(ServerError::InvalidUploadSession)
130        ));
131        assert!(matches!(
132            validate_upload_session_id(&"a".repeat(65)),
133            Err(ServerError::InvalidUploadSession)
134        ));
135    }
136
137    #[test]
138    fn oci_repository_scope_validator_accepts_bound_roots_and_nested_namespaces() {
139        let scope = RepositoryScope::new(RepositoryProvider::GitHub, "team", "assets", None);
140        assert!(scope.is_ok());
141        let Ok(scope) = scope else {
142            return;
143        };
144
145        assert!(validate_oci_repository_scope("team/assets", Some(&scope)).is_ok());
146        assert!(validate_oci_repository_scope("team/assets/cache", Some(&scope)).is_ok());
147        assert!(matches!(
148            validate_oci_repository_scope("team/other", Some(&scope)),
149            Err(ServerError::NotFound)
150        ));
151        assert!(matches!(
152            validate_oci_repository_scope("other/assets", Some(&scope)),
153            Err(ServerError::NotFound)
154        ));
155    }
156
157    #[test]
158    fn shared_sha256_key_uses_stable_shared_namespace() {
159        let key = shared_sha256_object_key(
160            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
161        );
162        assert!(key.is_ok());
163        let Ok(key) = key else {
164            return;
165        };
166        assert_eq!(
167            key.as_str(),
168            "protocols/shared/sha256/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
169        );
170    }
171}