composefs_ctl/
complete.rs1use std::ffi::OsStr;
2use std::path::{Path, PathBuf};
3
4use clap_complete::engine::CompletionCandidate;
5use composefs::fsverity::{FsVerityHashValue, Sha256HashValue, Sha512HashValue};
6use composefs::repository::Repository;
7use rustix::fs::CWD;
8
9use crate::HashType;
10
11fn resolve_repo_for_completion() -> Option<(PathBuf, HashType)> {
12 let args: Vec<String> = std::env::args().collect();
13 let mut repo_path = None;
14 let mut i = 0;
15 while i < args.len() {
16 match args[i].as_str() {
17 "--repo" => {
18 repo_path = args.get(i + 1).map(PathBuf::from);
19 i += 2;
20 }
21 _ if args[i].starts_with("--repo=") => {
22 repo_path = args[i].strip_prefix("--repo=").map(PathBuf::from);
23 i += 1;
24 }
25 "--user" => {
26 repo_path = composefs::repository::user_path().ok();
27 i += 1;
28 }
29 "--system" => {
30 repo_path = Some(composefs::repository::system_path());
31 i += 1;
32 }
33 _ => {
34 i += 1;
35 }
36 }
37 }
38 let repo_path = repo_path.or_else(|| crate::default_repo_path().ok())?;
39 let hash_type = crate::resolve_hash_type(&repo_path, None, false).ok()?;
40 Some((repo_path, hash_type))
41}
42
43#[cfg(feature = "oci")]
45pub fn complete_oci_tags(current: &OsStr) -> Vec<CompletionCandidate> {
46 let Some((repo_path, hash_type)) = resolve_repo_for_completion() else {
47 return vec![];
48 };
49 match hash_type {
50 HashType::Sha256 => collect_oci::<Sha256HashValue>(&repo_path, current, true, false),
51 HashType::Sha512 => collect_oci::<Sha512HashValue>(&repo_path, current, true, false),
52 }
53}
54
55#[cfg(feature = "oci")]
57pub fn complete_oci_digests(current: &OsStr) -> Vec<CompletionCandidate> {
58 let Some((repo_path, hash_type)) = resolve_repo_for_completion() else {
59 return vec![];
60 };
61 match hash_type {
62 HashType::Sha256 => collect_oci::<Sha256HashValue>(&repo_path, current, false, true),
63 HashType::Sha512 => collect_oci::<Sha512HashValue>(&repo_path, current, false, true),
64 }
65}
66
67#[cfg(feature = "oci")]
69pub fn complete_oci_tags_and_digests(current: &OsStr) -> Vec<CompletionCandidate> {
70 let Some((repo_path, hash_type)) = resolve_repo_for_completion() else {
71 return vec![];
72 };
73 match hash_type {
74 HashType::Sha256 => collect_oci::<Sha256HashValue>(&repo_path, current, true, true),
75 HashType::Sha512 => collect_oci::<Sha512HashValue>(&repo_path, current, true, true),
76 }
77}
78
79#[cfg(feature = "oci")]
80fn collect_oci<ObjectID: FsVerityHashValue>(
81 repo_path: &Path,
82 prefix: &OsStr,
83 tags: bool,
84 digests: bool,
85) -> Vec<CompletionCandidate> {
86 let Some(repo) = Repository::<ObjectID>::open_path(CWD, repo_path).ok() else {
87 return vec![];
88 };
89 let Ok(images) = composefs_oci::oci_image::list_images(&repo) else {
90 return vec![];
91 };
92 let prefix = prefix.as_encoded_bytes();
93 let mut out = Vec::new();
94 for img in &images {
95 if tags && img.name.as_bytes().starts_with(prefix) {
96 out.push(CompletionCandidate::new(&img.name));
97 }
98 if digests {
99 let d: &str = img.manifest_digest.as_ref();
100 if d.as_bytes().starts_with(prefix) {
101 out.push(CompletionCandidate::new(d));
102 }
103 }
104 }
105 out
106}
107
108#[cfg(feature = "ostree")]
110pub fn complete_ostree_refs(current: &OsStr) -> Vec<CompletionCandidate> {
111 let Some((repo_path, hash_type)) = resolve_repo_for_completion() else {
112 return vec![];
113 };
114 match hash_type {
115 HashType::Sha256 => collect_ostree_refs::<Sha256HashValue>(&repo_path, current),
116 HashType::Sha512 => collect_ostree_refs::<Sha512HashValue>(&repo_path, current),
117 }
118}
119
120#[cfg(feature = "ostree")]
121fn collect_ostree_refs<ObjectID: FsVerityHashValue>(
122 repo_path: &Path,
123 prefix: &OsStr,
124) -> Vec<CompletionCandidate> {
125 let Some(repo) = Repository::<ObjectID>::open_path(CWD, repo_path).ok() else {
126 return vec![];
127 };
128 let Ok(commits) = composefs_ostree::list_commits(&repo) else {
129 return vec![];
130 };
131 let prefix = prefix.as_encoded_bytes();
132 commits
133 .into_iter()
134 .filter(|c| c.name.as_bytes().starts_with(prefix))
135 .map(|c| CompletionCandidate::new(c.name))
136 .collect()
137}
138
139#[derive(Clone, Copy)]
140enum RefKind {
141 Image,
142 Stream,
143}
144
145pub fn complete_image_refs(current: &OsStr) -> Vec<CompletionCandidate> {
147 complete_refs(current, RefKind::Image)
148}
149
150pub fn complete_stream_refs(current: &OsStr) -> Vec<CompletionCandidate> {
152 complete_refs(current, RefKind::Stream)
153}
154
155fn complete_refs(current: &OsStr, kind: RefKind) -> Vec<CompletionCandidate> {
156 let Some((repo_path, hash_type)) = resolve_repo_for_completion() else {
157 return vec![];
158 };
159 match hash_type {
160 HashType::Sha256 => collect_refs::<Sha256HashValue>(&repo_path, current, kind),
161 HashType::Sha512 => collect_refs::<Sha512HashValue>(&repo_path, current, kind),
162 }
163}
164
165fn collect_refs<ObjectID: FsVerityHashValue>(
166 repo_path: &Path,
167 prefix: &OsStr,
168 kind: RefKind,
169) -> Vec<CompletionCandidate> {
170 let Some(repo) = Repository::<ObjectID>::open_path(CWD, repo_path).ok() else {
171 return vec![];
172 };
173 let refs = match kind {
174 RefKind::Image => repo.list_image_refs(""),
175 RefKind::Stream => repo.list_stream_refs(""),
176 };
177 let Ok(refs) = refs else {
178 return vec![];
179 };
180 let prefix = prefix.as_encoded_bytes();
181 let mut out = Vec::new();
182 for (name, _) in &refs {
183 if name.as_bytes().starts_with(prefix) {
184 out.push(CompletionCandidate::new(name));
185 }
186 let prefixed = format!("refs/{name}");
187 if prefixed.as_bytes().starts_with(prefix) {
188 out.push(CompletionCandidate::new(prefixed));
189 }
190 }
191 out
192}