1use std::{collections::HashMap, path::Path};
2
3use anyhow::{anyhow, Context, Result};
4use base64::Engine;
5use oci_distribution::secrets::RegistryAuth;
6use serde::Deserialize;
7
8#[derive(Deserialize)]
9struct AuthEntry {
10 auth: String,
11}
12
13impl AuthEntry {
14 pub fn extract_registry_auth(&self) -> Result<RegistryAuth> {
15 let decoded = base64::engine::general_purpose::STANDARD
16 .decode(&self.auth)
17 .with_context(|| anyhow!("Failed to decode base64"))?;
18 let decoded_str =
19 String::from_utf8(decoded).with_context(|| anyhow!("Invalid utf8 chars"))?;
20 let (user, pass) = decoded_str
21 .split_once(':')
22 .with_context(|| anyhow!("Unable to find `:` in the auth string"))?;
23 Ok(RegistryAuth::Basic(user.to_owned(), pass.to_owned()))
24 }
25}
26
27#[derive(Deserialize)]
28struct DockerConfig {
29 auths: HashMap<String, AuthEntry>,
30}
31
32pub trait RegistryAuthExt {
33 fn load_from_docker(path: Option<&Path>, registry: &str) -> Result<Self>
34 where
35 Self: Sized;
36
37 fn load_from_prompt() -> Result<Self>
38 where
39 Self: Sized;
40}
41
42impl RegistryAuthExt for RegistryAuth {
43 fn load_from_docker(path: Option<&Path>, registry: &str) -> Result<Self>
44 where
45 Self: Sized,
46 {
47 let docker_cfg_path = match path.map(|v| v.to_path_buf()) {
48 Some(v) => v,
49 None => home::home_dir()
50 .with_context(|| anyhow!("Unable to retrive home directory"))?
51 .join(".docker/config.json"),
52 };
53 let config: DockerConfig = serde_json::from_str(
54 &std::fs::read_to_string(&docker_cfg_path)
55 .with_context(|| anyhow!("Failed to read docker config"))?,
56 )
57 .with_context(|| anyhow!("Failed to deserialize docker config"))?;
58 let auth_entry = config.auths.get(registry).with_context(|| {
59 anyhow!(
60 "Unable to find auth entry named `{}` in {:?}",
61 registry,
62 docker_cfg_path
63 )
64 })?;
65 auth_entry.extract_registry_auth()
66 }
67 fn load_from_prompt() -> Result<Self>
68 where
69 Self: Sized,
70 {
71 print!("Username: ");
72 let mut username = String::default();
73 std::io::stdin().read_line(&mut username)?;
74 let password = rpassword::prompt_password("Password: ")?;
75 Ok(Self::Basic(username, password))
76 }
77}
78
79#[cfg(test)]
80mod tests {
81
82 use std::path::PathBuf;
83
84 use oci_distribution::secrets::RegistryAuth;
85 use tempfile::{tempdir, TempDir};
86
87 use super::RegistryAuthExt;
88
89 fn write_temp_file() -> (PathBuf, TempDir) {
90 let dir = tempdir().unwrap();
91 let data = include_bytes!("../assets/docker-config-test.json");
92 std::fs::write(dir.path().join("test.json"), data).unwrap();
93 (dir.path().join("test.json"), dir)
94 }
95
96 #[test]
97 fn test_load_docker_config() {
98 let (f, _dir) = write_temp_file();
99 let auth = RegistryAuth::load_from_docker(Some(&f), "ghcr.io").unwrap();
100 match auth {
101 RegistryAuth::Basic(a, b) => {
102 assert_eq!(a, "aaa");
103 assert_eq!(b, "bbb");
104 }
105 _ => unreachable!(),
106 }
107 }
108}