1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use rns_core::msgpack::{self, Value};
7use rns_net::RequestResponse;
8
9use crate::protocol;
10use crate::util::{hex, validate_repo_name};
11use crate::{Error, Result};
12
13#[derive(Debug, Clone)]
14pub struct ReleaseSummary {
15 pub tag: String,
16 pub hash: String,
17 pub created: u64,
18 pub status: String,
19 pub created_by: String,
20 pub preview: String,
21 pub preview_format: String,
22 pub artifacts: u64,
23}
24
25#[derive(Debug, Clone)]
26pub struct ReleaseData {
27 pub tag: String,
28 pub hash: String,
29 pub created: u64,
30 pub status: String,
31 pub created_by: String,
32 pub notes: String,
33 pub notes_format: String,
34 pub artifacts: Vec<Artifact>,
35 pub thanks: u64,
36}
37
38#[derive(Debug, Clone)]
39pub struct Artifact {
40 pub name: String,
41 pub size: u64,
42}
43
44#[derive(Debug, Clone)]
45pub struct ReleaseRequest {
46 pub repository: String,
47 pub operation: String,
48 pub tag: Option<String>,
49 pub step: Option<String>,
50 pub hash: Option<String>,
51 pub notes: Option<String>,
52 pub notes_format: Option<String>,
53 pub artifact_name: Option<String>,
54 pub artifact_data: Option<Vec<u8>>,
55 pub artifact: Option<String>,
56}
57
58pub fn parse_request(data: &[u8]) -> Result<ReleaseRequest> {
59 let value =
60 msgpack::unpack_exact(data).map_err(|e| Error::msg(format!("invalid msgpack: {e}")))?;
61 let map = value
62 .as_map()
63 .ok_or_else(|| Error::msg("request must be a msgpack map"))?;
64 let repository = map_get_repository(map)
65 .ok_or_else(|| Error::msg("request missing repository"))?
66 .to_string();
67 validate_repo_name(&repository)?;
68 let operation = map_get_str(map, "operation")
69 .ok_or_else(|| Error::msg("request missing operation"))?
70 .to_string();
71 Ok(ReleaseRequest {
72 repository,
73 operation,
74 tag: map_get_str(map, "tag").map(ToOwned::to_owned),
75 step: map_get_str(map, "step").map(ToOwned::to_owned),
76 hash: map_get_str(map, "hash").map(ToOwned::to_owned),
77 notes: map_get_str(map, "notes").map(ToOwned::to_owned),
78 notes_format: map_get_str(map, "notes_format").map(ToOwned::to_owned),
79 artifact_name: map_get_str(map, "artifact_name").map(ToOwned::to_owned),
80 artifact_data: map_get(map, "artifact_data").and_then(value_to_bytes),
81 artifact: map_get_str(map, "artifact").map(ToOwned::to_owned),
82 })
83}
84
85pub fn release_sidecar_path(repo: &Path) -> PathBuf {
86 sidecar_path(repo, "releases")
87}
88
89pub fn list_releases(releases_path: &Path) -> Result<Vec<ReleaseSummary>> {
90 let mut releases = Vec::new();
91 if !releases_path.is_dir() {
92 return Ok(releases);
93 }
94 for entry in fs::read_dir(releases_path)? {
95 let entry = entry?;
96 if !entry.file_type()?.is_dir() {
97 continue;
98 }
99 let release_dir = entry.path();
100 let meta_path = release_dir.join("META");
101 if !meta_path.is_file() {
102 continue;
103 }
104 let meta = read_meta(&meta_path)?;
105 let tag = meta_get(&meta, "tag")
106 .unwrap_or_else(|| entry.file_name().to_string_lossy().into_owned());
107 let (preview, preview_format) = notes_preview(&release_dir)?;
108 releases.push(ReleaseSummary {
109 tag,
110 hash: meta_get(&meta, "hash").unwrap_or_default(),
111 created: meta_get(&meta, "created")
112 .and_then(|v| v.parse().ok())
113 .unwrap_or(0),
114 status: meta_get(&meta, "status").unwrap_or_else(|| "unknown".to_string()),
115 created_by: meta_get(&meta, "created_by").unwrap_or_default(),
116 preview,
117 preview_format,
118 artifacts: artifact_count(&release_dir)?,
119 });
120 }
121 releases.sort_by(|a, b| b.created.cmp(&a.created));
122 Ok(releases)
123}
124
125pub fn release_data(releases_path: &Path, tag: &str) -> Result<Option<ReleaseData>> {
126 let tag = clean_tag(tag).ok_or_else(|| Error::msg("invalid tag name"))?;
127 let release_dir = releases_path.join(&tag);
128 if !release_dir.is_dir() {
129 return Ok(None);
130 }
131 let meta_path = release_dir.join("META");
132 if !meta_path.is_file() {
133 return Ok(None);
134 }
135 let meta = read_meta(&meta_path)?;
136 let (notes, notes_format) = read_notes(&release_dir)?;
137 Ok(Some(ReleaseData {
138 tag: meta_get(&meta, "tag").unwrap_or(tag),
139 hash: meta_get(&meta, "hash").unwrap_or_default(),
140 created: meta_get(&meta, "created")
141 .and_then(|v| v.parse().ok())
142 .unwrap_or(0),
143 status: meta_get(&meta, "status").unwrap_or_else(|| "unknown".to_string()),
144 created_by: meta_get(&meta, "created_by").unwrap_or_default(),
145 notes,
146 notes_format,
147 artifacts: artifacts(&release_dir)?,
148 thanks: thanks_count(&release_dir)?,
149 }))
150}
151
152pub fn latest_published_tag(releases_path: &Path) -> Result<Option<String>> {
153 if let Some(tag) = configured_latest_tag(releases_path)? {
154 return Ok(Some(tag));
155 }
156 Ok(list_releases(releases_path)?
157 .into_iter()
158 .filter(|release| release.status == "published")
159 .next()
160 .map(|release| release.tag))
161}
162
163pub fn configured_latest_tag(releases_path: &Path) -> Result<Option<String>> {
164 let latest_path = releases_path.join("latest");
165 if !latest_path.is_file() {
166 return Ok(None);
167 }
168 let tag = fs::read_to_string(latest_path)?.trim().to_string();
169 if tag.is_empty() {
170 return Ok(None);
171 }
172 let Some(tag) = clean_tag(&tag) else {
173 return Ok(None);
174 };
175 let Some(release) = release_data(releases_path, &tag)? else {
176 return Ok(None);
177 };
178 Ok((release.status == "published").then_some(release.tag))
179}
180
181pub fn create_init(
182 releases_path: &Path,
183 repo_path: &Path,
184 request: &ReleaseRequest,
185 remote: &[u8; 16],
186) -> Result<Vec<u8>> {
187 let Some(tag) = request.tag.as_deref().and_then(clean_tag) else {
188 return Ok(protocol::status_bytes(
189 protocol::RES_INVALID_REQ,
190 b"invalid tag name",
191 ));
192 };
193 if let Err(err) = verify_tag(repo_path, &tag) {
194 return Ok(protocol::status_bytes(
195 protocol::RES_INVALID_REQ,
196 err.to_string(),
197 ));
198 }
199 fs::create_dir_all(releases_path)?;
200 let release_dir = releases_path.join(&tag);
201 if release_dir.exists() {
202 return Ok(protocol::status_bytes(
203 protocol::RES_DISALLOWED,
204 b"release already exists",
205 ));
206 }
207 fs::create_dir_all(release_dir.join("artifacts"))?;
208 let created = unix_time();
209 let mut meta = vec![
210 ("tag".to_string(), tag.clone()),
211 ("created".to_string(), created.to_string()),
212 ("status".to_string(), "draft".to_string()),
213 ("created_by".to_string(), hex(remote)),
214 ];
215 if let Some(hash) = request.hash.as_deref().filter(|v| !v.is_empty()) {
216 meta.push(("hash".to_string(), hash.to_string()));
217 }
218 write_meta(&release_dir.join("META"), &meta)?;
219 if let Some(notes) = request.notes.as_deref().filter(|v| !v.is_empty()) {
220 let notes_file = match request.notes_format.as_deref() {
221 Some("micron") => "RELEASE.mu",
222 Some("text") => "RELEASE.txt",
223 _ => "RELEASE.md",
224 };
225 fs::write(release_dir.join(notes_file), notes)?;
226 }
227 write_thanks(&release_dir, 0)?;
228 Ok(protocol::status_bytes(protocol::RES_OK, b"ok"))
229}
230
231pub fn create_artifact(releases_path: &Path, request: &ReleaseRequest) -> Result<Vec<u8>> {
232 let Some(tag) = request.tag.as_deref().and_then(clean_tag) else {
233 return Ok(protocol::status_bytes(
234 protocol::RES_INVALID_REQ,
235 b"invalid tag name",
236 ));
237 };
238 let artifact_name = request
239 .artifact_name
240 .as_deref()
241 .and_then(clean_component)
242 .ok_or_else(|| Error::msg("invalid artifact name"))?;
243 let data = request
244 .artifact_data
245 .as_deref()
246 .ok_or_else(|| Error::msg("missing artifact data"))?;
247 let release_dir = releases_path.join(&tag);
248 if !release_dir.is_dir() {
249 return Ok(protocol::status_bytes(
250 protocol::RES_NOT_FOUND,
251 b"release not found",
252 ));
253 }
254 let meta_path = release_dir.join("META");
255 let meta = read_meta(&meta_path)?;
256 if meta_get(&meta, "status").as_deref() != Some("draft") {
257 return Ok(protocol::status_bytes(
258 protocol::RES_DISALLOWED,
259 b"release was finalized and is not writable",
260 ));
261 }
262 let artifacts_dir = release_dir.join("artifacts");
263 fs::create_dir_all(&artifacts_dir)?;
264 fs::write(artifacts_dir.join(artifact_name), data)?;
265 Ok(protocol::status_bytes(protocol::RES_OK, b"ok"))
266}
267
268pub fn create_finalize(releases_path: &Path, request: &ReleaseRequest) -> Result<Vec<u8>> {
269 let Some(tag) = request.tag.as_deref().and_then(clean_tag) else {
270 return Ok(protocol::status_bytes(
271 protocol::RES_INVALID_REQ,
272 b"invalid tag name",
273 ));
274 };
275 let release_dir = releases_path.join(&tag);
276 if !release_dir.is_dir() {
277 return Ok(protocol::status_bytes(
278 protocol::RES_NOT_FOUND,
279 b"release not found",
280 ));
281 }
282 let meta_path = release_dir.join("META");
283 let mut meta = read_meta(&meta_path)?;
284 if meta_get(&meta, "status").as_deref() != Some("draft") {
285 return Ok(protocol::status_bytes(
286 protocol::RES_DISALLOWED,
287 b"release was finalized and is not writable",
288 ));
289 }
290 set_meta(&mut meta, "status", "published");
291 set_meta(&mut meta, "published_at", &unix_time().to_string());
292 write_meta(&meta_path, &meta)?;
293 if let Err(err) = write_latest_marker(releases_path, &tag) {
294 log::error!(
295 "error setting latest release for {:?}: {}",
296 releases_path,
297 err
298 );
299 }
300 Ok(protocol::status_bytes(protocol::RES_OK, b"ok"))
301}
302
303pub fn delete_release(releases_path: &Path, request: &ReleaseRequest) -> Result<Vec<u8>> {
304 let Some(tag) = request.tag.as_deref().and_then(clean_tag) else {
305 return Ok(protocol::status_bytes(
306 protocol::RES_INVALID_REQ,
307 b"invalid tag name",
308 ));
309 };
310 let release_dir = releases_path.join(tag);
311 if !release_dir.is_dir() {
312 return Ok(protocol::status_bytes(
313 protocol::RES_NOT_FOUND,
314 b"release not found",
315 ));
316 }
317 fs::remove_dir_all(release_dir)?;
318 Ok(protocol::status_bytes(protocol::RES_OK, b"ok"))
319}
320
321pub fn set_latest_release(releases_path: &Path, request: &ReleaseRequest) -> Result<Vec<u8>> {
322 let Some(tag) = request.tag.as_deref().and_then(clean_tag) else {
323 return Ok(protocol::status_bytes(
324 protocol::RES_INVALID_REQ,
325 b"invalid tag name",
326 ));
327 };
328 let release_dir = releases_path.join(&tag);
329 if !release_dir.is_dir() {
330 return Ok(protocol::status_bytes(
331 protocol::RES_NOT_FOUND,
332 b"release not found",
333 ));
334 }
335 fs::create_dir_all(releases_path)?;
336 write_latest_marker(releases_path, &tag)?;
337 Ok(protocol::status_bytes(protocol::RES_OK, b"ok"))
338}
339
340pub fn list_response(releases_path: &Path) -> Result<Vec<u8>> {
341 let releases = Value::Array(
342 list_releases(releases_path)?
343 .into_iter()
344 .map(summary_value)
345 .collect(),
346 );
347 let latest = configured_latest_tag(releases_path)?
348 .map(Value::Str)
349 .unwrap_or(Value::Nil);
350 Ok(protocol::status_bytes(
351 protocol::RES_OK,
352 msgpack::pack(&Value::Map(vec![
353 (Value::Str("releases".into()), releases),
354 (Value::Str("latest".into()), latest),
355 ])),
356 ))
357}
358
359pub fn view_response(releases_path: &Path, tag: &str) -> Result<Vec<u8>> {
360 let resolved_tag;
361 let tag = if tag == "latest" {
362 let Some(tag) = latest_published_tag(releases_path)? else {
363 return Ok(protocol::status_bytes(
364 protocol::RES_NOT_FOUND,
365 b"no latest release found",
366 ));
367 };
368 resolved_tag = tag;
369 resolved_tag.as_str()
370 } else {
371 tag
372 };
373 let release = match release_data(releases_path, tag) {
374 Ok(release) => release,
375 Err(err) if err.to_string() == "invalid tag name" => {
376 return Ok(protocol::status_bytes(
377 protocol::RES_INVALID_REQ,
378 b"invalid tag name",
379 ));
380 }
381 Err(err) => return Err(err),
382 };
383 match release {
384 Some(release) => Ok(protocol::status_bytes(
385 protocol::RES_OK,
386 msgpack::pack(&data_value(release)),
387 )),
388 None => Ok(protocol::status_bytes(
389 protocol::RES_NOT_FOUND,
390 b"release not found",
391 )),
392 }
393}
394
395pub fn fetch_response(releases_path: &Path, request: &ReleaseRequest) -> Result<RequestResponse> {
396 let Some(tag) = request.tag.as_deref() else {
397 return Ok(RequestResponse::Bytes(protocol::status_bytes(
398 protocol::RES_INVALID_REQ,
399 b"invalid tag name",
400 )));
401 };
402 let tag = if tag == "latest" {
403 let Some(tag) = latest_published_tag(releases_path)? else {
404 return Ok(RequestResponse::Bytes(protocol::status_bytes(
405 protocol::RES_NOT_FOUND,
406 b"no latest release found",
407 )));
408 };
409 tag
410 } else if let Some(tag) = clean_tag(tag) {
411 tag
412 } else {
413 return Ok(RequestResponse::Bytes(protocol::status_bytes(
414 protocol::RES_INVALID_REQ,
415 b"invalid tag name",
416 )));
417 };
418 let Some(raw_artifact) = request.artifact.as_deref() else {
419 return Ok(RequestResponse::Bytes(protocol::status_bytes(
420 protocol::RES_INVALID_REQ,
421 b"invalid artifact name",
422 )));
423 };
424 if raw_artifact.contains('/') || raw_artifact.contains('\\') {
425 return Ok(RequestResponse::Bytes(protocol::status_bytes(
426 protocol::RES_INVALID_REQ,
427 b"invalid artifact name",
428 )));
429 }
430 let Some(artifact) = clean_component(raw_artifact) else {
431 return Ok(RequestResponse::Bytes(protocol::status_bytes(
432 protocol::RES_INVALID_REQ,
433 b"invalid artifact name",
434 )));
435 };
436 let Some(release) = release_data(releases_path, &tag)? else {
437 return Ok(RequestResponse::Bytes(protocol::status_bytes(
438 protocol::RES_NOT_FOUND,
439 b"release not found",
440 )));
441 };
442 if release.status != "published" {
443 return Ok(RequestResponse::Bytes(protocol::status_bytes(
444 protocol::RES_NOT_FOUND,
445 b"release not found",
446 )));
447 }
448 let path = releases_path.join(tag).join("artifacts").join(&artifact);
449 if !path.is_file() {
450 return Ok(RequestResponse::Bytes(protocol::status_bytes(
451 protocol::RES_NOT_FOUND,
452 b"artifact not found",
453 )));
454 }
455 let data = fs::read(path)?;
456 Ok(RequestResponse::Resource {
457 data,
458 metadata: Some(release_fetch_metadata(&artifact)),
459 auto_compress: true,
460 })
461}
462
463fn release_fetch_metadata(name: &str) -> Vec<u8> {
464 msgpack::pack(&Value::Map(vec![
465 (
466 Value::UInt(protocol::IDX_RESULT_CODE),
467 Value::UInt(protocol::RES_OK as u64),
468 ),
469 (Value::Str("name".into()), Value::Str(name.to_string())),
470 ]))
471}
472
473pub fn release_thanks(release_dir: &Path, add: bool) -> Result<u64> {
474 let mut count = thanks_count(release_dir)?;
475 if add {
476 count = count.saturating_add(1);
477 write_thanks(release_dir, count)?;
478 }
479 Ok(count)
480}
481
482pub fn artifact_path(releases_path: &Path, tag: &str, artifact: &str) -> Result<Option<PathBuf>> {
483 let tag = if tag == "latest" {
484 let Some(tag) = latest_published_tag(releases_path)? else {
485 return Ok(None);
486 };
487 tag
488 } else {
489 clean_tag(tag).ok_or_else(|| Error::msg("invalid tag name"))?
490 };
491 let artifact = clean_component(artifact).ok_or_else(|| Error::msg("invalid artifact name"))?;
492 let Some(release) = release_data(releases_path, &tag)? else {
493 return Ok(None);
494 };
495 if release.status != "published" {
496 return Ok(None);
497 }
498 let path = releases_path.join(tag).join("artifacts").join(artifact);
499 Ok(path.is_file().then_some(path))
500}
501
502fn summary_value(release: ReleaseSummary) -> Value {
503 Value::Map(vec![
504 (Value::Str("tag".into()), Value::Str(release.tag)),
505 (Value::Str("hash".into()), Value::Str(release.hash)),
506 (Value::Str("created".into()), Value::UInt(release.created)),
507 (Value::Str("status".into()), Value::Str(release.status)),
508 (
509 Value::Str("created_by".into()),
510 Value::Str(release.created_by),
511 ),
512 (Value::Str("preview".into()), Value::Str(release.preview)),
513 (
514 Value::Str("preview_format".into()),
515 Value::Str(release.preview_format),
516 ),
517 (
518 Value::Str("artifacts".into()),
519 Value::UInt(release.artifacts),
520 ),
521 ])
522}
523
524fn data_value(release: ReleaseData) -> Value {
525 Value::Map(vec![
526 (Value::Str("tag".into()), Value::Str(release.tag)),
527 (Value::Str("hash".into()), Value::Str(release.hash)),
528 (Value::Str("created".into()), Value::UInt(release.created)),
529 (Value::Str("status".into()), Value::Str(release.status)),
530 (
531 Value::Str("created_by".into()),
532 Value::Str(release.created_by),
533 ),
534 (Value::Str("notes".into()), Value::Str(release.notes)),
535 (
536 Value::Str("notes_format".into()),
537 Value::Str(release.notes_format),
538 ),
539 (
540 Value::Str("artifacts".into()),
541 Value::Array(release.artifacts.into_iter().map(artifact_value).collect()),
542 ),
543 (Value::Str("thanks".into()), Value::UInt(release.thanks)),
544 ])
545}
546
547fn write_latest_marker(releases_path: &Path, tag: &str) -> Result<()> {
548 let latest_path = releases_path.join("latest");
549 let tmp_path = releases_path.join("latest.tmp");
550 fs::write(&tmp_path, tag)?;
551 fs::rename(tmp_path, latest_path)?;
552 Ok(())
553}
554
555fn artifact_value(artifact: Artifact) -> Value {
556 Value::Map(vec![
557 (Value::Str("name".into()), Value::Str(artifact.name)),
558 (Value::Str("size".into()), Value::UInt(artifact.size)),
559 ])
560}
561
562fn sidecar_path(repo: &Path, extension: &str) -> PathBuf {
563 let mut name = repo.as_os_str().to_os_string();
564 name.push(format!(".{extension}"));
565 PathBuf::from(name)
566}
567
568fn read_meta(path: &Path) -> Result<Vec<(String, String)>> {
569 let input = fs::read_to_string(path)?;
570 let mut out = Vec::new();
571 for raw in input.lines() {
572 let line = raw.trim();
573 if line.is_empty() || line.starts_with('#') {
574 continue;
575 }
576 if let Some((key, value)) = line.split_once('=') {
577 out.push((key.trim().to_string(), value.trim().to_string()));
578 }
579 }
580 Ok(out)
581}
582
583fn write_meta(path: &Path, values: &[(String, String)]) -> Result<()> {
584 let mut out = String::new();
585 for (key, value) in values {
586 out.push_str(key);
587 out.push_str(" = ");
588 out.push_str(value);
589 out.push('\n');
590 }
591 fs::write(path, out)?;
592 Ok(())
593}
594
595fn meta_get(values: &[(String, String)], key: &str) -> Option<String> {
596 values
597 .iter()
598 .find_map(|(k, v)| (k == key).then(|| v.clone()))
599}
600
601fn set_meta(values: &mut Vec<(String, String)>, key: &str, value: &str) {
602 if let Some((_, existing)) = values.iter_mut().find(|(k, _)| k == key) {
603 *existing = value.to_string();
604 } else {
605 values.push((key.to_string(), value.to_string()));
606 }
607}
608
609fn read_notes(release_dir: &Path) -> Result<(String, String)> {
610 for (file, format) in [
611 ("RELEASE.md", "markdown"),
612 ("RELEASE.mu", "micron"),
613 ("RELEASE.txt", "text"),
614 ] {
615 let path = release_dir.join(file);
616 if path.is_file() {
617 return Ok((fs::read_to_string(path)?, format.to_string()));
618 }
619 }
620 Ok((String::new(), "text".to_string()))
621}
622
623fn notes_preview(release_dir: &Path) -> Result<(String, String)> {
624 let (notes, format) = read_notes(release_dir)?;
625 let first = notes
626 .lines()
627 .map(str::trim)
628 .find(|line| !line.is_empty() && !line.starts_with('#') && !line.starts_with('>'))
629 .unwrap_or("");
630 Ok((first.chars().take(256).collect(), format))
631}
632
633fn artifacts(release_dir: &Path) -> Result<Vec<Artifact>> {
634 let artifacts_dir = release_dir.join("artifacts");
635 let mut out = Vec::new();
636 if !artifacts_dir.is_dir() {
637 return Ok(out);
638 }
639 for entry in fs::read_dir(artifacts_dir)? {
640 let entry = entry?;
641 if !entry.file_type()?.is_file() {
642 continue;
643 }
644 out.push(Artifact {
645 name: entry.file_name().to_string_lossy().into_owned(),
646 size: entry.metadata()?.len(),
647 });
648 }
649 out.sort_by(|a, b| a.name.cmp(&b.name));
650 Ok(out)
651}
652
653fn artifact_count(release_dir: &Path) -> Result<u64> {
654 Ok(artifacts(release_dir)?.len() as u64)
655}
656
657fn thanks_count(release_dir: &Path) -> Result<u64> {
658 let path = release_dir.join("THANKS");
659 if !path.is_file() {
660 return Ok(0);
661 }
662 let bytes = fs::read(path)?;
663 if let Ok(value) = msgpack::unpack_exact(&bytes) {
664 return Ok(value
665 .map_get("count")
666 .and_then(Value::as_integer)
667 .unwrap_or(0)
668 .max(0) as u64);
669 }
670 Ok(String::from_utf8_lossy(&bytes).trim().parse().unwrap_or(0))
671}
672
673fn write_thanks(release_dir: &Path, count: u64) -> Result<()> {
674 fs::write(
675 release_dir.join("THANKS"),
676 msgpack::pack(&Value::Map(vec![(
677 Value::Str("count".into()),
678 Value::UInt(count),
679 )])),
680 )?;
681 Ok(())
682}
683
684fn verify_tag(repo_path: &Path, tag: &str) -> Result<()> {
685 let output = Command::new("git")
686 .arg("--git-dir")
687 .arg(repo_path)
688 .arg("rev-parse")
689 .arg("--verify")
690 .arg(format!("refs/tags/{tag}"))
691 .output()?;
692 if output.status.success() {
693 Ok(())
694 } else {
695 Err(Error::msg(format!(
696 "tag '{tag}' does not exist in repository"
697 )))
698 }
699}
700
701fn unix_time() -> u64 {
702 SystemTime::now()
703 .duration_since(UNIX_EPOCH)
704 .unwrap_or_default()
705 .as_secs()
706}
707
708fn clean_component(value: &str) -> Option<String> {
709 let name = Path::new(value).file_name()?.to_str()?.trim();
710 (!name.is_empty() && name != "." && name != "..").then(|| name.to_string())
711}
712
713fn clean_tag(value: &str) -> Option<String> {
714 if value.contains('/') || value.contains('\\') {
715 return None;
716 }
717 clean_component(value)
718}
719
720fn map_get_repository<'a>(map: &'a [(Value, Value)]) -> Option<&'a str> {
721 map.iter().find_map(|(key, value)| match key {
722 Value::UInt(v) if *v == protocol::IDX_REPOSITORY => value.as_str(),
723 Value::Str(v) if v == "repository" => value.as_str(),
724 _ => None,
725 })
726}
727
728fn map_get<'a>(map: &'a [(Value, Value)], key: &str) -> Option<&'a Value> {
729 map.iter().find_map(|(k, v)| match k {
730 Value::Str(s) if s == key => Some(v),
731 _ => None,
732 })
733}
734
735fn map_get_str<'a>(map: &'a [(Value, Value)], key: &str) -> Option<&'a str> {
736 map_get(map, key).and_then(Value::as_str)
737}
738
739fn value_to_bytes(value: &Value) -> Option<Vec<u8>> {
740 match value {
741 Value::Bin(bytes) => Some(bytes.clone()),
742 Value::Str(text) => Some(text.as_bytes().to_vec()),
743 _ => None,
744 }
745}