Rust implementation of OCI Distribution Spec with granular access control
1use axum::body::Body;
2use std::{
3 fs::{create_dir_all, File},
4 io::Write,
5};
6
7pub(crate) fn sanitize_string(input: &str) -> String {
8 input
9 .chars()
10 .map(|c| {
11 if c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-' || c == '/' {
12 c
13 } else {
14 '_'
15 }
16 })
17 .collect()
18}
19
20pub(crate) async fn write_blob(org: &str, repo: &str, req_digest_string: &str, body: Body) -> bool {
21 let bytes_res = axum::body::to_bytes(body, usize::MAX).await;
22 if bytes_res.is_err() {
23 return false;
24 }
25 let bytes = bytes_res.unwrap();
26
27 let req_digest = req_digest_string
28 .strip_prefix("sha256:")
29 .unwrap_or(req_digest_string);
30 let body_digest = sha256::digest(bytes.as_ref());
31 let matches = req_digest == body_digest;
32
33 log::info!(
34 "storage/write_file: digest: {}, body_digest: {}, matches: {}",
35 req_digest,
36 body_digest,
37 matches
38 );
39
40 if !matches {
41 return false;
42 }
43
44 let base_path = format!(
45 "./tmp/blobs/{}/{}",
46 sanitize_string(org),
47 sanitize_string(repo),
48 );
49
50 write_bytes_to_file(&base_path, req_digest, &bytes).await
51}
52
53pub(crate) async fn write_manifest_bytes(
54 org: &str,
55 repo: &str,
56 reference: &str,
57 bytes: &[u8],
58) -> bool {
59 let base_path = format!(
60 "./tmp/manifests/{}/{}",
61 sanitize_string(org),
62 sanitize_string(repo),
63 );
64
65 write_bytes_to_file(&base_path, reference, bytes).await
66}
67
68pub(crate) async fn write_bytes_to_file(base_path: &str, file_name: &str, bytes: &[u8]) -> bool {
69 if let Err(e) = create_dir_all(base_path) {
70 log::error!("storage/write_file: error creating directory: {}", e);
71 return false;
72 }
73
74 let mut file = match File::create(format!("{}/{}", base_path, file_name)) {
75 Ok(file) => file,
76 Err(e) => {
77 log::error!("storage/write_file: error creating file: {}", e);
78 return false;
79 }
80 };
81
82 if let Err(e) = file.write_all(bytes) {
83 log::error!("storage/write_file: error writing to file: {}", e);
84 return false;
85 }
86
87 if let Err(e) = file.flush() {
88 log::error!("storage/write_file: error flushing file: {}", e);
89 return false;
90 }
91
92 log::info!("storage/write_file: wrote to {}", base_path);
93
94 true
95}
96
97pub(crate) fn read_blob(org: &str, repo: &str, digest: &str) -> Result<Vec<u8>, std::io::Error> {
98 let sanitized_org = sanitize_string(org);
99 let sanitized_repo = sanitize_string(repo);
100 let sanitized_digest = sanitize_string(digest);
101
102 let blob_path = format!(
103 "./tmp/blobs/{}/{}/{}",
104 sanitized_org, sanitized_repo, sanitized_digest
105 );
106 std::fs::read(blob_path)
107}
108
109pub(crate) fn blob_metadata(
110 org: &str,
111 repo: &str,
112 digest: &str,
113) -> Result<std::fs::Metadata, std::io::Error> {
114 let sanitized_org = sanitize_string(org);
115 let sanitized_repo = sanitize_string(repo);
116 let sanitized_digest = sanitize_string(digest);
117
118 let blob_path = format!(
119 "./tmp/blobs/{}/{}/{}",
120 sanitized_org, sanitized_repo, sanitized_digest
121 );
122 std::fs::metadata(blob_path)
123}
124
125pub(crate) fn read_manifest(
126 org: &str,
127 repo: &str,
128 reference: &str,
129) -> Result<Vec<u8>, std::io::Error> {
130 let sanitized_org = sanitize_string(org);
131 let sanitized_repo = sanitize_string(repo);
132 let sanitized_reference = sanitize_string(reference);
133
134 let manifest_path = format!(
135 "./tmp/manifests/{}/{}/{}",
136 sanitized_org, sanitized_repo, sanitized_reference
137 );
138 std::fs::read(manifest_path)
139}
140
141pub(crate) fn manifest_exists(org: &str, repo: &str, reference: &str) -> bool {
142 let sanitized_org = sanitize_string(org);
143 let sanitized_repo = sanitize_string(repo);
144 let sanitized_reference = sanitize_string(reference);
145
146 let manifest_path = format!(
147 "./tmp/manifests/{}/{}/{}",
148 sanitized_org, sanitized_repo, sanitized_reference
149 );
150 std::path::Path::new(&manifest_path).exists()
151}
152
153pub(crate) fn list_tags(org: &str, repo: &str) -> Result<Vec<String>, std::io::Error> {
154 let sanitized_org = sanitize_string(org);
155 let sanitized_repo = sanitize_string(repo);
156
157 let manifests_dir = format!("./tmp/manifests/{}/{}", sanitized_org, sanitized_repo);
158 let path = std::path::Path::new(&manifests_dir);
159
160 if !path.exists() {
161 return Ok(Vec::new());
162 }
163
164 let mut tags = Vec::new();
165
166 for entry in std::fs::read_dir(path)? {
167 let entry = entry?;
168 if entry.path().is_file() {
169 if let Some(filename) = entry.file_name().to_str() {
170 // Filter out digest references (64-char hex strings or sha256: prefixed)
171 // Only include tag names
172 let is_digest = filename.starts_with("sha256:")
173 || (filename.len() == 64 && filename.chars().all(|c| c.is_ascii_hexdigit()));
174
175 if !is_digest {
176 tags.push(filename.to_string());
177 }
178 }
179 }
180 }
181
182 // Sort tags alphabetically for consistent ordering
183 tags.sort();
184 Ok(tags)
185}
186
187pub(crate) fn init_upload_session(org: &str, repo: &str, uuid: &str) -> Result<(), std::io::Error> {
188 let sanitized_org = sanitize_string(org);
189 let sanitized_repo = sanitize_string(repo);
190 let sanitized_uuid = sanitize_string(uuid);
191
192 let upload_dir = format!("./tmp/uploads/{}/{}", sanitized_org, sanitized_repo);
193 std::fs::create_dir_all(&upload_dir)?;
194
195 let upload_path = format!("{}/{}", upload_dir, sanitized_uuid);
196 std::fs::File::create(upload_path)?;
197 Ok(())
198}
199
200pub(crate) fn append_upload_chunk(
201 org: &str,
202 repo: &str,
203 uuid: &str,
204 chunk_data: &[u8],
205) -> Result<u64, std::io::Error> {
206 use std::fs::OpenOptions;
207
208 let sanitized_org = sanitize_string(org);
209 let sanitized_repo = sanitize_string(repo);
210 let sanitized_uuid = sanitize_string(uuid);
211
212 let upload_path = format!(
213 "./tmp/uploads/{}/{}/{}",
214 sanitized_org, sanitized_repo, sanitized_uuid
215 );
216
217 let mut file = OpenOptions::new().append(true).open(&upload_path)?;
218
219 file.write_all(chunk_data)?;
220
221 let metadata = std::fs::metadata(&upload_path)?;
222 Ok(metadata.len())
223}
224
225pub(crate) fn finalize_upload(
226 org: &str,
227 repo: &str,
228 uuid: &str,
229 expected_digest: &str,
230) -> Result<String, String> {
231 let sanitized_org = sanitize_string(org);
232 let sanitized_repo = sanitize_string(repo);
233 let sanitized_uuid = sanitize_string(uuid);
234
235 let upload_path = format!(
236 "./tmp/uploads/{}/{}/{}",
237 sanitized_org, sanitized_repo, sanitized_uuid
238 );
239
240 let upload_data =
241 std::fs::read(&upload_path).map_err(|e| format!("Failed to read upload: {}", e))?;
242
243 let actual_digest = sha256::digest(&upload_data);
244 let clean_expected = expected_digest
245 .strip_prefix("sha256:")
246 .unwrap_or(expected_digest);
247
248 if actual_digest != clean_expected {
249 return Err(format!(
250 "Digest mismatch: expected {}, got {}",
251 clean_expected, actual_digest
252 ));
253 }
254
255 let blob_dir = format!("./tmp/blobs/{}/{}", sanitized_org, sanitized_repo);
256 std::fs::create_dir_all(&blob_dir).map_err(|e| format!("Failed to create blob dir: {}", e))?;
257
258 let blob_path = format!("{}/{}", blob_dir, actual_digest);
259 std::fs::rename(&upload_path, &blob_path)
260 .map_err(|e| format!("Failed to move upload to blob: {}", e))?;
261
262 Ok(actual_digest)
263}
264
265pub(crate) fn delete_upload_session(
266 org: &str,
267 repo: &str,
268 uuid: &str,
269) -> Result<(), std::io::Error> {
270 let sanitized_org = sanitize_string(org);
271 let sanitized_repo = sanitize_string(repo);
272 let sanitized_uuid = sanitize_string(uuid);
273
274 let upload_path = format!(
275 "./tmp/uploads/{}/{}/{}",
276 sanitized_org, sanitized_repo, sanitized_uuid
277 );
278 std::fs::remove_file(upload_path)
279}
280
281pub(crate) fn delete_manifest(
282 org: &str,
283 repo: &str,
284 reference: &str,
285) -> Result<(), std::io::Error> {
286 let sanitized_org = sanitize_string(org);
287 let sanitized_repo = sanitize_string(repo);
288 let sanitized_reference = sanitize_string(reference);
289
290 let manifest_path = format!(
291 "./tmp/manifests/{}/{}/{}",
292 sanitized_org, sanitized_repo, sanitized_reference
293 );
294
295 if !std::path::Path::new(&manifest_path).exists() {
296 return Err(std::io::Error::new(
297 std::io::ErrorKind::NotFound,
298 "Manifest not found",
299 ));
300 }
301
302 std::fs::remove_file(manifest_path)
303}
304
305pub(crate) fn delete_blob(org: &str, repo: &str, digest: &str) -> Result<(), std::io::Error> {
306 let sanitized_org = sanitize_string(org);
307 let sanitized_repo = sanitize_string(repo);
308 let sanitized_digest = sanitize_string(digest);
309
310 let blob_path = format!(
311 "./tmp/blobs/{}/{}/{}",
312 sanitized_org, sanitized_repo, sanitized_digest
313 );
314
315 if !std::path::Path::new(&blob_path).exists() {
316 return Err(std::io::Error::new(
317 std::io::ErrorKind::NotFound,
318 "Blob not found",
319 ));
320 }
321
322 std::fs::remove_file(blob_path)
323}
324
325pub(crate) fn mount_blob(
326 source_org: &str,
327 source_repo: &str,
328 target_org: &str,
329 target_repo: &str,
330 digest: &str,
331) -> Result<(), std::io::Error> {
332 let sanitized_source_org = sanitize_string(source_org);
333 let sanitized_source_repo = sanitize_string(source_repo);
334 let sanitized_target_org = sanitize_string(target_org);
335 let sanitized_target_repo = sanitize_string(target_repo);
336 let sanitized_digest = sanitize_string(digest);
337
338 // Check if blob exists in source repository
339 let source_path = format!(
340 "./tmp/blobs/{}/{}/{}",
341 sanitized_source_org, sanitized_source_repo, sanitized_digest
342 );
343
344 if !std::path::Path::new(&source_path).exists() {
345 return Err(std::io::Error::new(
346 std::io::ErrorKind::NotFound,
347 "Source blob not found",
348 ));
349 }
350
351 // Create target directory
352 let target_dir = format!(
353 "./tmp/blobs/{}/{}",
354 sanitized_target_org, sanitized_target_repo
355 );
356 std::fs::create_dir_all(&target_dir)?;
357
358 // Create target path
359 let target_path = format!("{}/{}", target_dir, sanitized_digest);
360
361 // If target already exists, that's fine (already mounted)
362 if std::path::Path::new(&target_path).exists() {
363 return Ok(());
364 }
365
366 // Try hard link first (most efficient - no data duplication)
367 if std::fs::hard_link(&source_path, &target_path).is_err() {
368 // If hard link fails (cross-device), copy the file
369 std::fs::copy(&source_path, &target_path)?;
370 }
371
372 Ok(())
373}