Skip to main content

composefs_ctl/
complete.rs

1use 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/// Complete OCI image tag names from the active repository.
44#[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/// Complete OCI manifest digests from the active repository.
56#[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/// Complete OCI image tags and manifest digests from the active repository.
68#[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/// Complete ostree commit references from the active repository.
109#[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
145/// Complete image names (`refs/…`) from the active repository.
146pub fn complete_image_refs(current: &OsStr) -> Vec<CompletionCandidate> {
147    complete_refs(current, RefKind::Image)
148}
149
150/// Complete stream names (`refs/…`) from the active repository.
151pub 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}