shardline_server/
protocol_support.rs1use 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}