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