1use anyhow::Result;
13use std::fmt::Write;
14
15fn escape_for_ref(s: &str) -> Result<String> {
17 if s.is_empty() {
18 return Err(anyhow::anyhow!("Invalid empty string for ref"));
19 }
20 fn escape_c(r: &mut String, c: char) {
21 write!(r, "_{:02X}_", c as u32).unwrap()
22 }
23 let mut r = String::new();
24 let mut it = s
25 .chars()
26 .map(|c| {
27 if c == '\0' {
28 Err(anyhow::anyhow!(
29 "Invalid embedded NUL in string for ostree ref"
30 ))
31 } else {
32 Ok(c)
33 }
34 })
35 .peekable();
36
37 let mut previous_alphanumeric = false;
38 while let Some(c) = it.next() {
39 let has_next = it.peek().is_some();
40 let c = c?;
41 let current_alphanumeric = c.is_ascii_alphanumeric();
42 match c {
43 c if current_alphanumeric => r.push(c),
44 '/' if previous_alphanumeric && has_next => r.push(c),
45 '-' => r.push(c),
47 '_' => r.push_str("__"),
49 o => escape_c(&mut r, o),
50 }
51 previous_alphanumeric = current_alphanumeric;
52 }
53 Ok(r)
54}
55
56pub fn prefix_escape_for_ref(prefix: &str, s: &str) -> Result<String> {
78 Ok(format!("{}/{}", prefix, escape_for_ref(s)?))
79}
80
81fn unescape_for_ref(s: &str) -> Result<String> {
83 let mut r = String::new();
84 let mut it = s.chars();
85 let mut buf = String::new();
86 while let Some(c) = it.next() {
87 match c {
88 c if c.is_ascii_alphanumeric() => {
89 r.push(c);
90 }
91 '-' | '/' => r.push(c),
92 '_' => {
93 let next = it.next();
94 if let Some('_') = next {
95 r.push('_')
96 } else if let Some(c) = next {
97 buf.clear();
98 buf.push(c);
99 for c in &mut it {
100 if c == '_' {
101 break;
102 }
103 buf.push(c);
104 }
105 let v = u32::from_str_radix(&buf, 16)?;
106 let c: char = v.try_into()?;
107 r.push(c);
108 }
109 }
110 o => anyhow::bail!("Invalid character {}", o),
111 }
112 }
113 Ok(r)
114}
115
116pub fn unprefix_unescape_ref(prefix: &str, ostree_ref: &str) -> Result<String> {
129 let rest = ostree_ref
130 .strip_prefix(prefix)
131 .and_then(|s| s.strip_prefix('/'))
132 .ok_or_else(|| {
133 anyhow::anyhow!(
134 "ref does not match expected prefix {}/: {}",
135 ostree_ref,
136 prefix
137 )
138 })?;
139 unescape_for_ref(rest)
140}
141
142#[cfg(test)]
143mod test {
144 use super::*;
145 use quickcheck::{TestResult, quickcheck};
146
147 const TESTPREFIX: &str = "testprefix/blah";
148
149 const UNCHANGED: &[&str] = &["foo", "foo/bar/baz-blah/foo"];
150 const ROUNDTRIP: &[&str] = &[
151 "localhost:5000/foo:latest",
152 "fedora/x86_64/coreos",
153 "/foo/bar/foo.oci-archive",
154 "/foo/bar/foo.docker-archive",
155 "docker://quay.io/exampleos/blah:latest",
156 "oci-archive:/path/to/foo.ociarchive",
157 "docker-archive:/path/to/foo.dockerarchive",
158 ];
159 const CORNERCASES: &[&str] = &["/", "blah/", "/foo/"];
160
161 #[test]
162 fn escape() {
163 for &v in UNCHANGED {
165 let escaped = &escape_for_ref(v).unwrap();
166 ostree::validate_rev(escaped).unwrap();
167 assert_eq!(escaped.as_str(), v);
168 }
169 for &v in UNCHANGED.iter().chain(ROUNDTRIP).chain(CORNERCASES) {
171 let escaped = &prefix_escape_for_ref(TESTPREFIX, v).unwrap();
172 ostree::validate_rev(escaped).unwrap();
173 let unescaped = unprefix_unescape_ref(TESTPREFIX, escaped).unwrap();
174 assert_eq!(v, unescaped);
175 }
176 assert_eq!(
178 escape_for_ref(ROUNDTRIP[0]).unwrap(),
179 "localhost_3A_5000/foo_3A_latest"
180 );
181 }
182
183 fn roundtrip(s: String) -> TestResult {
184 let r = prefix_escape_for_ref(TESTPREFIX, &s);
186 let escaped = match r {
187 Ok(v) => v,
188 Err(_) => return TestResult::discard(),
189 };
190 let unescaped = unprefix_unescape_ref(TESTPREFIX, &escaped).unwrap();
191 TestResult::from_bool(unescaped == s)
192 }
193
194 #[test]
195 fn qcheck() {
196 quickcheck(roundtrip as fn(String) -> TestResult);
197 }
198}