1use std::{
2 borrow::Cow,
3 ffi::OsString,
4 ops::Deref,
5 path::{Path, PathBuf},
6 str::FromStr,
7};
8
9use lazy_regex::regex;
10use miette::miette;
11use oci_client::Reference;
12use serde::{Deserialize, Serialize};
13
14use crate::platform::Platform;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ContainerId(pub String);
18
19impl Deref for ContainerId {
20 type Target = str;
21
22 fn deref(&self) -> &Self::Target {
23 &self.0
24 }
25}
26
27impl std::fmt::Display for ContainerId {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 write!(f, "{}", &self.0)
30 }
31}
32
33impl AsRef<std::ffi::OsStr> for ContainerId {
34 fn as_ref(&self) -> &std::ffi::OsStr {
35 self.0.as_ref()
36 }
37}
38
39pub struct MountId(pub String);
40
41impl Deref for MountId {
42 type Target = str;
43
44 fn deref(&self) -> &Self::Target {
45 &self.0
46 }
47}
48
49impl std::fmt::Display for MountId {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 write!(f, "{}", &self.0)
52 }
53}
54
55impl AsRef<std::ffi::OsStr> for MountId {
56 fn as_ref(&self) -> &std::ffi::OsStr {
57 self.0.as_ref()
58 }
59}
60
61impl<'a> From<&'a MountId> for std::borrow::Cow<'a, str> {
62 fn from(value: &'a MountId) -> Self {
63 Self::Borrowed(&value.0)
64 }
65}
66
67#[derive(Clone, Debug)]
68pub enum OciRef {
69 LocalStorage(Reference),
70 OciArchive(PathBuf),
71 OciDir(PathBuf),
72 Remote(Reference),
73}
74
75impl std::fmt::Display for OciRef {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 Self::LocalStorage(local_ref) => write!(f, "containers-storage:{}", local_ref.whole()),
79 Self::OciArchive(path) => write!(f, "oci-archive:{}", path.display()),
80 Self::OciDir(path) => write!(f, "oci:{}", path.display()),
81 Self::Remote(image_ref) => write!(f, "docker://{}", image_ref.whole()),
82 }
83 }
84}
85
86impl OciRef {
87 #[must_use]
88 pub fn from_local_storage(local_ref: &Reference) -> Self {
89 Self::LocalStorage(local_ref.clone())
90 }
91
92 pub fn from_oci_archive<P: AsRef<Path>>(path: P) -> Result<Self, miette::Report> {
95 if !path.as_ref().is_file() {
96 miette::bail!("OCI archive doesn't exist at {}", path.as_ref().display());
97 }
98
99 Ok(Self::OciArchive(path.as_ref().to_owned()))
100 }
101
102 pub fn from_oci_directory<P: AsRef<Path>>(path: P) -> Result<Self, miette::Report> {
105 if !path.as_ref().is_dir() {
106 miette::bail!("OCI directory doesn't exist at {}", path.as_ref().display());
107 }
108
109 Ok(Self::OciDir(path.as_ref().to_owned()))
110 }
111
112 #[must_use]
113 pub fn from_remote_ref(image_ref: &Reference) -> Self {
114 Self::Remote(image_ref.clone())
115 }
116
117 #[must_use]
118 pub fn to_os_string(&self) -> OsString {
119 match self {
120 Self::LocalStorage(local_ref) => format!("containers-storage:{local_ref}").into(),
121 Self::OciArchive(path) => {
122 let mut out = OsString::from("oci-archive:");
123 out.push(path.as_os_str());
124 out
125 }
126 Self::OciDir(path) => {
127 let mut out = OsString::from("oci:");
128 out.push(path.as_os_str());
129 out
130 }
131 Self::Remote(image_ref) => format!("docker://{}", image_ref.whole()).into(),
132 }
133 }
134}
135
136#[derive(Debug, Clone)]
139pub enum ImageRef<'scope> {
140 Remote(Cow<'scope, Reference>),
141 LocalTar(Cow<'scope, Path>),
142 Other(Cow<'scope, str>),
143}
144
145impl<'scope> ImageRef<'scope> {
146 #[must_use]
147 pub fn remote_ref(&self) -> Option<&Reference> {
148 match self {
149 Self::Remote(remote) => Some(remote.as_ref()),
150 _ => None,
151 }
152 }
153
154 #[must_use]
155 pub fn with_platform(&'scope self, platform: Platform) -> Self {
156 if let Self::Remote(remote) = &self {
157 Self::Remote(Cow::Owned(platform.tagged_image(remote)))
158 } else if let Self::LocalTar(path) = &self
159 && let Some(tagged) = platform.tagged_path(path)
160 {
161 Self::LocalTar(Cow::Owned(tagged))
162 } else {
163 Self::from(self)
164 }
165 }
166
167 #[must_use]
172 pub fn append_tag(&self, value: &Tag) -> Self {
173 match self {
174 Self::Remote(image) => Self::Remote(Cow::Owned(Reference::with_tag(
175 image.registry().to_owned(),
176 image.repository().to_owned(),
177 image
178 .tag()
179 .map_or_else(|| format!("latest_{value}"), |tag| format!("{tag}_{value}")),
180 ))),
181 Self::LocalTar(path) => {
182 if let Some(file_stem) = path.file_stem()
183 && let Some(extension) = path.extension()
184 {
185 Self::LocalTar(Cow::Owned(
186 path.with_file_name(format!("{}_{value}", file_stem.display(),))
187 .with_extension(extension),
188 ))
189 } else {
190 Self::LocalTar(Cow::Owned(PathBuf::from(format!(
191 "{}_{value}",
192 path.display()
193 ))))
194 }
195 }
196 Self::Other(other) => Self::Other(Cow::Owned(format!("{other}_{value}"))),
197 }
198 }
199}
200
201impl<'scope> From<&'scope Self> for ImageRef<'scope> {
202 fn from(value: &'scope ImageRef) -> Self {
203 match value {
204 Self::Remote(remote) => Self::Remote(Cow::Borrowed(remote.as_ref())),
205 Self::LocalTar(path) => Self::LocalTar(Cow::Borrowed(path.as_ref())),
206 Self::Other(other) => Self::Other(Cow::Borrowed(other.as_ref())),
207 }
208 }
209}
210
211impl<'scope> From<&'scope Reference> for ImageRef<'scope> {
212 fn from(value: &'scope Reference) -> Self {
213 Self::Remote(Cow::Borrowed(value))
214 }
215}
216
217impl From<Reference> for ImageRef<'_> {
218 fn from(value: Reference) -> Self {
219 Self::Remote(Cow::Owned(value))
220 }
221}
222
223impl<'scope> From<&'scope Path> for ImageRef<'scope> {
224 fn from(value: &'scope Path) -> Self {
225 Self::LocalTar(Cow::Borrowed(value))
226 }
227}
228
229impl<'scope> From<&'scope PathBuf> for ImageRef<'scope> {
230 fn from(value: &'scope PathBuf) -> Self {
231 Self::from(value.as_path())
232 }
233}
234
235impl From<PathBuf> for ImageRef<'_> {
236 fn from(value: PathBuf) -> Self {
237 Self::LocalTar(Cow::Owned(value))
238 }
239}
240
241impl From<ImageRef<'_>> for String {
242 fn from(value: ImageRef<'_>) -> Self {
243 Self::from(&value)
244 }
245}
246
247impl From<&ImageRef<'_>> for String {
248 fn from(value: &ImageRef<'_>) -> Self {
249 format!("{value}")
250 }
251}
252
253impl std::fmt::Display for ImageRef<'_> {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 write!(
256 f,
257 "{}",
258 match self {
259 Self::Remote(remote) => remote.whole(),
260 Self::LocalTar(path) => format!("oci-archive:{}", path.display()),
261 Self::Other(other) => other.to_string(),
262 }
263 )
264 }
265}
266
267impl PartialEq<Reference> for ImageRef<'_> {
268 fn eq(&self, other: &Reference) -> bool {
269 match self {
270 Self::Remote(remote) => &**remote == other,
271 _ => false,
272 }
273 }
274}
275
276#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)]
277pub struct Tag(String);
278
279impl Tag {
280 #[must_use]
281 pub fn as_str(&self) -> &str {
282 &self.0
283 }
284}
285
286impl FromStr for Tag {
287 type Err = miette::Error;
288
289 fn from_str(s: &str) -> Result<Self, Self::Err> {
290 let regex = regex!(r"[\w][\w.-]{0,127}");
291 regex
292 .is_match(s)
293 .then(|| Self(s.into()))
294 .ok_or_else(|| miette!("Invalid tag: {s}"))
295 }
296}
297
298impl std::fmt::Display for Tag {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 write!(f, "{}", &self.0)
301 }
302}
303
304impl TryFrom<&Reference> for Tag {
305 type Error = miette::Error;
306
307 fn try_from(value: &Reference) -> Result<Self, Self::Error> {
308 value
309 .tag()
310 .map(|tag| Self(tag.into()))
311 .ok_or_else(|| miette!("Reference {value} has no tag"))
312 }
313}
314
315impl<'de> Deserialize<'de> for Tag {
316 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
317 where
318 D: serde::Deserializer<'de>,
319 {
320 Self::from_str(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
321 }
322}
323
324impl Default for Tag {
325 fn default() -> Self {
326 Self(String::from("latest"))
327 }
328}
329
330impl From<Tag> for String {
331 fn from(value: Tag) -> Self {
332 value.0
333 }
334}
335
336impl From<&Tag> for String {
337 fn from(value: &Tag) -> Self {
338 value.0.clone()
339 }
340}