This repository has been archived by the owner on Nov 7, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Prep for work on the new container module, where we want to store container image references (e.g. `docker://quay.io/coreos/fedora`) as ostree refs. Several bits of that are not valid in ostree refs, such as the `:` or the double `//` (which would be an empty filesystem path). This escaping scheme uses `_` in a similar way as a `\` character is used in other syntax. For example, `:` is `_3A_` (hexadecimal). `//` is escaped as `/_2F_` (i.e. the second `/` is escaped).
- Loading branch information
Showing
3 changed files
with
195 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
//! Escape strings for use in ostree refs. | ||
//! | ||
//! It can be desirable to map arbitrary identifiers, such as RPM/dpkg | ||
//! package names or container image references (e.g. `docker://quay.io/examplecorp/os:latest`) | ||
//! into ostree refs (branch names) which have a quite restricted set | ||
//! of valid characters; basically alphanumeric, plus `/`, `-`, `_`. | ||
//! | ||
//! This escaping scheme uses `_` in a similar way as a `\` character is | ||
//! used in Rust unicode escaped values. For example, `:` is `_3A_` (hexadecimal). | ||
//! Because the empty path is not valid, `//` is escaped as `/_2F_` (i.e. the second `/` is escaped). | ||
|
||
use anyhow::Result; | ||
use std::convert::TryInto; | ||
use std::fmt::Write; | ||
|
||
/// Escape a string in a suitable fashion for an ostree ref. | ||
/// | ||
/// There are just a few restrictions: | ||
/// - The empty string is not supported | ||
/// - There may not be embedded `NUL` (`\\0`) characters. | ||
fn escape_for_ref(s: &str) -> Result<String> { | ||
if s.is_empty() { | ||
return Err(anyhow::anyhow!("Invalid empty string for ref")); | ||
} | ||
fn escape_c(r: &mut String, c: char) { | ||
write!(r, "_{:02X}_", c as u32).unwrap() | ||
} | ||
let mut r = String::new(); | ||
let mut it = s.chars().peekable(); | ||
|
||
// We require a leading alphanumeric | ||
match it.peek() { | ||
Some(&c) if !c.is_ascii_alphanumeric() => { | ||
escape_c(&mut r, c); | ||
let _ = it.next(); | ||
} | ||
_ => {} | ||
} | ||
|
||
while let Some(c) = it.next() { | ||
match (c, it.peek().copied()) { | ||
('\0', _) => { | ||
return Err(anyhow::anyhow!( | ||
"Invalid embedded NUL in string for ostree ref" | ||
)); | ||
} | ||
(c, _) if c.is_ascii_alphanumeric() => { | ||
r.push(c); | ||
} | ||
// The `/` must be followed by alphanumeric | ||
('/', Some(next)) if !next.is_ascii_alphanumeric() => { | ||
let _ = it.next(); | ||
r.push('/'); | ||
escape_c(&mut r, next); | ||
} | ||
// Also can't end on a `/` per above. | ||
('/', None) => { | ||
escape_c(&mut r, '/'); | ||
} | ||
// Pass through - and `/` as valid | ||
('-' | '/', _) => r.push(c), | ||
// `_` becomes `__`. | ||
('_', _) => r.push_str("__"), | ||
// Otherwise, escape. | ||
(o, _) => escape_c(&mut r, o), | ||
} | ||
} | ||
Ok(r) | ||
} | ||
|
||
/// Compute a string suitable for use as an OSTree ref, where `s` can be a (nearly) | ||
/// arbitrary UTF-8 string. This requires a non-empty prefix. | ||
/// | ||
/// The intention behind requiring a prefix is that a common need is to use e.g. | ||
/// [`ostree::Repo::list_refs`] to find refs of a certain "type". | ||
/// | ||
/// ```rust | ||
/// # fn test() -> anyhow::Result<()> { | ||
/// use ostree_ext::refescape; | ||
/// assert_eq!(refescape::prefix_escape_for_ref("container", "registry:quay.io/coreos/fedora:latest")?, "container/registry_3A_quay_2E_io/coreos/fedora_3A_latest"); | ||
/// # Ok(()) | ||
/// } | ||
/// ``` | ||
pub fn prefix_escape_for_ref(prefix: &str, s: &str) -> Result<String> { | ||
Ok(format!("{}/{}", prefix, escape_for_ref(s)?)) | ||
} | ||
|
||
/// Reverse the effect of [`escape_for_ref()`]. | ||
fn unescape_for_ref(s: &str) -> Result<String> { | ||
let mut r = String::new(); | ||
let mut it = s.chars(); | ||
let mut buf = String::new(); | ||
while let Some(c) = it.next() { | ||
match c { | ||
c if c.is_ascii_alphanumeric() => { | ||
r.push(c); | ||
} | ||
'-' | '/' => r.push(c), | ||
'_' => { | ||
let next = it.next(); | ||
if let Some('_') = next { | ||
r.push('_') | ||
} else if let Some(c) = next { | ||
buf.clear(); | ||
buf.push(c); | ||
while let Some(c) = it.next() { | ||
if c == '_' { | ||
break; | ||
} | ||
buf.push(c); | ||
} | ||
let v = u32::from_str_radix(&buf, 16)?; | ||
let c: char = v.try_into()?; | ||
r.push(c); | ||
} | ||
} | ||
o => anyhow::bail!("Invalid character {}", o), | ||
} | ||
} | ||
Ok(r) | ||
} | ||
|
||
/// Remove a prefix from an ostree ref, and return the unescaped remainder. | ||
/// | ||
/// ```rust | ||
/// # fn test() -> anyhow::Result<()> { | ||
/// use ostree_ext::refescape; | ||
/// assert_eq!(refescape::unprefix_unescape_ref("container", "container/registry_3A_quay_2E_io/coreos/fedora_3A_latest")?, "registry:quay.io/coreos/fedora:latest"); | ||
/// # Ok(()) | ||
/// } | ||
/// ``` | ||
pub fn unprefix_unescape_ref(prefix: &str, ostree_ref: &str) -> Result<String> { | ||
let rest = ostree_ref.strip_prefix(prefix).ok_or_else(|| { | ||
anyhow::anyhow!( | ||
"ref does not match expected prefix {}: {}", | ||
ostree_ref, | ||
prefix | ||
) | ||
})?; | ||
Ok(unescape_for_ref(rest)?) | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
use quickcheck::{quickcheck, TestResult}; | ||
|
||
const UNCHANGED: &[&str] = &["foo", "foo/bar/baz-blah/foo"]; | ||
const ROUNDTRIP: &[&str] = &[ | ||
"localhost:5000/foo:latest", | ||
"fedora/x86_64/coreos", | ||
"/foo/bar/foo.oci-archive", | ||
"docker://quay.io/exampleos/blah:latest", | ||
"oci-archive:/path/to/foo.ociarchive", | ||
]; | ||
|
||
#[test] | ||
fn escape() { | ||
// These strings shouldn't change | ||
for &v in UNCHANGED { | ||
let escaped = &escape_for_ref(v).unwrap(); | ||
ostree::validate_rev(escaped).unwrap(); | ||
assert_eq!(escaped.as_str(), v); | ||
} | ||
// Roundtrip cases, plus unchanged cases | ||
for &v in UNCHANGED.iter().chain(ROUNDTRIP) { | ||
let escaped = &escape_for_ref(v).unwrap(); | ||
ostree::validate_rev(escaped).unwrap(); | ||
let unescaped = unescape_for_ref(&escaped).unwrap(); | ||
assert_eq!(v, unescaped); | ||
} | ||
// Explicit test | ||
assert_eq!( | ||
escape_for_ref(ROUNDTRIP[0]).unwrap(), | ||
"localhost_3A_5000/foo_3A_latest" | ||
); | ||
} | ||
|
||
fn roundtrip(s: String) -> TestResult { | ||
// Ensure we only try strings which match the predicates. | ||
let escaped = match escape_for_ref(&s) { | ||
Ok(v) => v, | ||
Err(_) => return TestResult::discard(), | ||
}; | ||
let unescaped = unescape_for_ref(&escaped).unwrap(); | ||
TestResult::from_bool(unescaped == s) | ||
} | ||
|
||
#[test] | ||
fn qcheck() { | ||
quickcheck(roundtrip as fn(String) -> TestResult); | ||
} | ||
} |