Commit

Author:

Hash:

Timestamp:

+156 -136 +/-3 browse

Kevin Schoon [me@kevinschoon.com]

0e3fbf5f72aede30334209a33e110034df16a23b

Thu, 17 Apr 2025 12:20:08 +0000 (1 month ago)

further improve namespace and address parsing
1diff --git a/src/address.rs b/src/address.rs
2index 49fbe28..342dde5 100644
3--- a/src/address.rs
4+++ b/src/address.rs
5 @@ -11,6 +11,8 @@ use uuid::Uuid;
6
7 use crate::{Namespace, SEPARATOR};
8
9+ const STOP_WORDS: [&str; 4] = ["_layers", "_manifests", "_tags", "_tmp"];
10+
11 fn digest_prefix(digest: &Digest) -> &str {
12 match digest.algorithm() {
13 oci_spec::image::DigestAlgorithm::Sha256 => "sha256",
14 @@ -73,21 +75,39 @@ impl Address {
15 self.components().last().is_some_and(|part| part == "data")
16 }
17
18+ fn segment_ns(
19+ parts: impl IntoIterator<Item = String>,
20+ ) -> Result<(Namespace, Vec<String>), crate::error::Error> {
21+ let parts: Vec<String> = parts.into_iter().map(|item| item.to_string()).collect();
22+ let index = parts
23+ .iter()
24+ .position(|part| STOP_WORDS.contains(&part.as_str()));
25+ if let Some(index) = index {
26+ let ns_parts = &parts[..index];
27+ let namespace = Namespace::try_from(ns_parts)?;
28+ let rest = parts[index..].to_vec();
29+ Ok((namespace, rest))
30+ } else {
31+ let namespace = Namespace::try_from(parts.as_slice())?;
32+ Ok((namespace, Vec::new()))
33+ }
34+ }
35+
36 pub fn components(&self) -> Vec<String> {
37 match self {
38 Address::Tag { namespace, name } => Vec::from_iter(
39- format!("repositories/{}/tags/{}/current/link", namespace, name)
40+ format!("repositories/{}/_tags/{}/current/link", namespace, name)
41 .split("/")
42 .map(|part| part.to_string()),
43 ),
44 Address::TagDirectory { namespace } => Vec::from_iter(
45- format!("repositories/{}/tags", namespace)
46+ format!("repositories/{}/_tags", namespace)
47 .split("/")
48 .map(|part| part.to_string()),
49 ),
50 Address::Reference { namespace, digest } => Vec::from_iter(
51 format!(
52- "repositories/{}/manifests/revisions/{}/{}/link",
53+ "repositories/{}/_manifests/revisions/{}/{}/link",
54 namespace,
55 digest_prefix(digest),
56 digest
57 @@ -96,7 +116,7 @@ impl Address {
58 .map(|part| part.to_string()),
59 ),
60 Address::TempBlob { uuid, namespace } => Vec::from_iter(
61- format!("repositories/{}/tmp/{}", namespace, uuid)
62+ format!("repositories/{}/_tmp/{}", namespace, uuid)
63 .split("/")
64 .map(|part| part.to_string()),
65 ),
66 @@ -116,7 +136,7 @@ impl Address {
67 }
68 Address::ManifestRevision { namespace, digest } => Vec::from_iter(
69 format!(
70- "repositories/{}/manifests/revisions/{}/{}/link",
71+ "repositories/{}/_manifests/revisions/{}/{}/link",
72 namespace,
73 digest_prefix(digest),
74 digest.digest()
75 @@ -126,7 +146,7 @@ impl Address {
76 ),
77 Address::LayerLink { namespace, digest } => Vec::from_iter(
78 format!(
79- "repositories/{}/layers/{}/{}/link",
80+ "repositories/{}/_layers/{}/{}/link",
81 namespace,
82 digest_prefix(digest),
83 digest.digest()
84 @@ -159,49 +179,30 @@ impl FromStr for Address {
85 Ok(Address::Blob { digest })
86 }
87 "repositories" => {
88- if s.ends_with("tags") {
89+ let (namespace, rest) = Address::segment_ns(split)?;
90+ // println!("{} {:?}", namespace, rest);
91+ if rest.len() == 1 && rest[0] == "_tags" {
92 // example: /{namespace}/tags
93- split.pop_back();
94- let namespace = Namespace::try_from(
95- split.iter().cloned().collect::<Vec<String>>().as_slice(),
96- )?;
97 Ok(Address::TagDirectory { namespace })
98- } else if s.ends_with("current/link") {
99- // example: /{namespace}/tags/{name}/current/link
100- let name = split.get(split.len() - 3).cloned().unwrap();
101- split.truncate(split.len() - 4);
102- let namespace = Namespace::try_from(
103- split.iter().cloned().collect::<Vec<String>>().as_slice(),
104- )?;
105- Ok(Address::Tag { namespace, name })
106- } else if split.len() > 2
107- && split.get(split.len() - 2).is_some_and(|item| item == "tmp")
108+ } else if rest.len() > 2
109+ && rest[0] == "_tags"
110+ && rest[2] == "current"
111+ && rest[3] == "link"
112 {
113+ // example: /{namespace}/tags/{name}/current/link
114+ Ok(Address::Tag {
115+ namespace,
116+ name: rest[1].to_string(),
117+ })
118+ } else if rest.len() > 1 && rest[0] == "_tmp" {
119 // example: /hello/world/tmp/614f0a00-169e-4586-a6ed-cc52894130ae
120- let uuid = Uuid::from_str(&split.pop_back().unwrap())?;
121- split.pop_back();
122- let namespace = Namespace::try_from(
123- split.iter().cloned().collect::<Vec<String>>().as_slice(),
124- )?;
125- Ok(Address::TempBlob { uuid, namespace })
126- } else if split.len() > 4
127- && split
128- .get(split.len() - 4)
129- .is_some_and(|item| item == "revisions")
130- {
131+ let uuid = Uuid::from_str(&rest[1])?;
132+ Ok(Address::TempBlob { namespace, uuid })
133+ } else if rest.len() > 3 && rest[0] == "_manifests" {
134 // example: /hello/world/manifests/revisions/sha256/57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f/link
135- let alogrithm = split.get(split.len() - 3).unwrap();
136- let digest = split.get(split.len() - 2).unwrap();
137- let digest = Digest::from_str(&format!("{}:{}", alogrithm, digest))?;
138- split.truncate(split.len() - 5);
139- let namespace = Namespace::try_from(
140- split.iter().cloned().collect::<Vec<String>>().as_slice(),
141- )?;
142+ let digest = Digest::from_str(&format!("{}:{}", rest[2], rest[3]))?;
143 Ok(Address::ManifestRevision { namespace, digest })
144 } else {
145- let namespace = Namespace::try_from(
146- split.iter().cloned().collect::<Vec<String>>().as_slice(),
147- )?;
148 Ok(Address::Repository { namespace })
149 }
150 }
151 @@ -241,14 +242,14 @@ mod test {
152 &Address::TagDirectory {
153 namespace: namespace.clone(),
154 },
155- "repositories/hello/world/tags",
156+ "repositories/hello/world/_tags",
157 );
158 check_address(
159 &Address::Tag {
160 namespace: namespace.clone(),
161 name: String::from("latest"),
162 },
163- "repositories/hello/world/tags/latest/current/link",
164+ "repositories/hello/world/_tags/latest/current/link",
165 );
166 let uuid = Uuid::new_v4();
167 check_address(
168 @@ -256,7 +257,7 @@ mod test {
169 uuid,
170 namespace: namespace.clone(),
171 },
172- &format!("repositories/hello/world/tmp/{}", uuid),
173+ &format!("repositories/hello/world/_tmp/{}", uuid),
174 );
175 let digest = Digest::from_str(
176 "sha256:57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f",
177 @@ -273,7 +274,7 @@ mod test {
178 namespace: namespace.clone(),
179 digest: digest.clone(),
180 },
181- "repositories/hello/world/manifests/revisions/sha256/57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f/link",
182+ "repositories/hello/world/_manifests/revisions/sha256/57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f/link",
183 );
184 check_address(
185 &Address::Repository {
186 @@ -295,13 +296,14 @@ mod test {
187 });
188
189 [
190- "repositories/tags",
191+ "repositories/__fuu",
192+ "repositories/_tags",
193 "repositories/__/revisions/INVALID/INVALID/link",
194 ]
195 .iter()
196 .for_each(|item| {
197 if let Ok(addr) = Address::from_str(item) {
198- panic!("Item {} should be unparsable, got: {}", item, addr)
199+ panic!("Item {} should be unparsable, got: {:?}", item, addr)
200 }
201 })
202 // todo
203 diff --git a/src/lib.rs b/src/lib.rs
204index 22a5984..4eab7aa 100644
205--- a/src/lib.rs
206+++ b/src/lib.rs
207 @@ -1,16 +1,14 @@
208- use std::iter::Iterator;
209- use std::{fmt::Display, str::FromStr};
210-
211 use error::Error;
212 pub use oci_spec::image::Digest;
213- use regex::Regex;
214- use relative_path::{RelativePath, RelativePathBuf};
215
216 pub mod address;
217 pub mod error;
218+ pub mod namespace;
219 pub mod oci_interface;
220 pub mod storage;
221
222+ pub use crate::namespace::Namespace;
223+
224 #[cfg(feature = "axum")]
225 pub mod axum;
226
227 @@ -19,91 +17,8 @@ pub mod storage_fs;
228
229 pub const SEPARATOR: &str = "/";
230
231- const NAME_REGEXP_MATCH: &str =
232- r"[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*";
233-
234 #[derive(Clone, Debug)]
235 pub enum TagOrDigest {
236 Tag(String),
237 Digest(Digest),
238 }
239-
240- // TODO: Consider 255 char namespace limit - hostname length per spec docs
241- #[derive(Clone, Debug, PartialEq, Eq)]
242- pub struct Namespace(Vec<String>);
243-
244- impl Namespace {
245- pub fn path(&self) -> RelativePathBuf {
246- RelativePath::new(&self.0.join(SEPARATOR)).to_relative_path_buf()
247- }
248-
249- pub fn name(&self) -> String {
250- self.0.last().cloned().unwrap()
251- }
252- }
253-
254- impl TryFrom<&[&str]> for Namespace {
255- type Error = Error;
256-
257- fn try_from(value: &[&str]) -> Result<Self, Self::Error> {
258- Namespace::from_str(value.join(SEPARATOR).as_str())
259- }
260- }
261-
262- impl TryFrom<&[String]> for Namespace {
263- type Error = Error;
264-
265- fn try_from(value: &[String]) -> Result<Self, Self::Error> {
266- Namespace::from_str(value.join(SEPARATOR).as_str())
267- }
268- }
269-
270- impl FromStr for Namespace {
271- type Err = Error;
272-
273- fn from_str(s: &str) -> Result<Self, Self::Err> {
274- let regexp = Regex::new(NAME_REGEXP_MATCH).unwrap();
275- if regexp.is_match(s) {
276- Ok(Namespace(
277- s.split(SEPARATOR).map(|part| part.to_string()).collect(),
278- ))
279- } else {
280- Err(Error::Namespace(s.to_string()))
281- }
282- }
283- }
284-
285- impl IntoIterator for Namespace {
286- type Item = String;
287- type IntoIter = std::vec::IntoIter<String>;
288-
289- fn into_iter(self) -> Self::IntoIter {
290- self.0.into_iter()
291- }
292- }
293-
294- impl Display for Namespace {
295- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296- write!(f, "{}", self.0.join(SEPARATOR))
297- }
298- }
299-
300- // impl AsRef<str> for Namespace {
301- // fn as_ref(&self) -> &str {
302- // &self.0.join(SEPARATOR)
303- // }
304- // }
305-
306- #[cfg(test)]
307- mod test {
308- use super::*;
309-
310- #[test]
311- fn namespace() {
312- Namespace::from_str("fuu").unwrap();
313- Namespace::from_str("fuu/bar").unwrap();
314- Namespace::from_str("fuu/bar/baz/").unwrap();
315- Namespace::from_str("fuu/bar/baz/qux").unwrap();
316- Namespace::try_from(vec!["fuu", "bar"].as_slice()).unwrap();
317- }
318- }
319 diff --git a/src/namespace.rs b/src/namespace.rs
320new file mode 100644
321index 0000000..b06f587
322--- /dev/null
323+++ b/src/namespace.rs
324 @@ -0,0 +1,103 @@
325+ use std::iter::Iterator;
326+ use std::{fmt::Display, str::FromStr};
327+
328+ use regex::Regex;
329+ use relative_path::{RelativePath, RelativePathBuf};
330+
331+ use crate::SEPARATOR;
332+ use crate::error::Error;
333+
334+ // NOTE: I can't really find clear guidance on exactly what is and is not supported.
335+ const NAME_REGEXP_MATCH: &str = r"^[a-z0-9]+(?:[._-][a-z0-9]+)*";
336+
337+ // TODO: Consider 255 char namespace limit - hostname length per spec docs
338+ #[derive(Clone, Debug, PartialEq, Eq)]
339+ pub struct Namespace(Vec<String>);
340+
341+ impl Namespace {
342+ pub fn path(&self) -> RelativePathBuf {
343+ RelativePath::new(&self.0.join(SEPARATOR)).to_relative_path_buf()
344+ }
345+
346+ pub fn name(&self) -> String {
347+ self.0.last().cloned().unwrap()
348+ }
349+
350+ fn to_parts<'a>(
351+ path: &str,
352+ parts: &mut impl Iterator<Item = &'a str>,
353+ ) -> Result<Vec<String>, Error> {
354+ let regexp = Regex::new(NAME_REGEXP_MATCH).unwrap();
355+ parts.try_fold(Vec::new(), |mut accm, part| {
356+ if part.is_empty() {
357+ return Ok(accm);
358+ }
359+ if !regexp.is_match(part) {
360+ return Err(Error::Namespace(path.to_string()));
361+ };
362+ accm.push(part.to_string());
363+ Ok(accm)
364+ })
365+ }
366+ }
367+
368+ impl TryFrom<&[&str]> for Namespace {
369+ type Error = Error;
370+
371+ fn try_from(value: &[&str]) -> Result<Self, Self::Error> {
372+ Namespace::from_str(value.join(SEPARATOR).as_str())
373+ }
374+ }
375+
376+ impl TryFrom<&[String]> for Namespace {
377+ type Error = Error;
378+
379+ fn try_from(value: &[String]) -> Result<Self, Self::Error> {
380+ Namespace::from_str(value.join(SEPARATOR).as_str())
381+ }
382+ }
383+
384+ impl FromStr for Namespace {
385+ type Err = Error;
386+
387+ fn from_str(s: &str) -> Result<Self, Self::Err> {
388+ let parts = Namespace::to_parts(s, &mut s.split(SEPARATOR))?;
389+ if parts.is_empty() {
390+ return Err(Error::Namespace(s.to_string()))
391+ }
392+ Ok(Namespace(parts))
393+ }
394+ }
395+
396+ impl IntoIterator for Namespace {
397+ type Item = String;
398+ type IntoIter = std::vec::IntoIter<String>;
399+
400+ fn into_iter(self) -> Self::IntoIter {
401+ self.0.into_iter()
402+ }
403+ }
404+
405+ impl Display for Namespace {
406+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407+ write!(f, "{}", self.0.join(SEPARATOR))
408+ }
409+ }
410+
411+ #[cfg(test)]
412+ mod test {
413+ use super::*;
414+
415+ #[test]
416+ fn namespace() {
417+ Namespace::from_str("fuu").unwrap();
418+ Namespace::from_str("fuu/bar").unwrap();
419+ Namespace::from_str("fuu/bar/baz/").unwrap();
420+ Namespace::from_str("fuu/bar/baz/qux").unwrap();
421+ Namespace::from_str("fuu/b_ar/baz/qux").unwrap();
422+ Namespace::try_from(vec!["fuu", "bar"].as_slice()).unwrap();
423+ assert!(Namespace::from_str("fuu/_layers/bar").is_err());
424+ assert!(Namespace::from_str("").is_err());
425+ assert!(Namespace::try_from(Vec::<String>::new().as_slice()).is_err());
426+ }
427+ }