Skip to content
This repository has been archived by the owner on Nov 7, 2024. It is now read-only.

Commit

Permalink
Add new refescape module
Browse files Browse the repository at this point in the history
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
cgwalters committed Sep 29, 2021
1 parent 3657f8f commit 452215c
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ tracing = "0.1"
[dev-dependencies]
clap = "2.33.3"
indoc = "1.0.3"
quickcheck = "1"
sh-inline = "0.1.0"
structopt = "0.3.21"

Expand Down
1 change: 1 addition & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub mod cli;
pub mod container;
pub mod diff;
pub mod ima;
pub mod refescape;
pub mod tar;
pub mod tokio_util;

Expand Down
198 changes: 198 additions & 0 deletions lib/src/refescape.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
//! 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 single string; this is a backend of [`prefix_escape_for_ref`].
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()
.map(|c| {
if c == '\0' {
Err(anyhow::anyhow!(
"Invalid embedded NUL in string for ostree ref"
))
} else {
Ok(c)
}
})
.peekable();

let mut previous_alphanumeric = false;
while let Some(c) = it.next() {
let has_next = it.peek().is_some();
let c = c?;
let current_alphanumeric = c.is_ascii_alphanumeric();
match c {
c if current_alphanumeric => r.push(c),
'/' if previous_alphanumeric && has_next => r.push(c),
// Pass through `-` unconditionally
'-' => r.push(c),
// The underscore `_` quotes itself `__`.
'_' => r.push_str("__"),
o => escape_c(&mut r, o),
}
previous_alphanumeric = current_alphanumeric;
}
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 restrictions on `s` are:
/// - The empty string is not supported
/// - There may not be embedded `NUL` (`\0`) characters.
///
/// 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".
///
/// # Examples:
///
/// ```rust
/// # fn test() -> anyhow::Result<()> {
/// use ostree_ext::refescape;
/// let s = "registry:quay.io/coreos/fedora:latest";
/// assert_eq!(refescape::prefix_escape_for_ref("container", s)?,
/// "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.
///
/// # Examples:
///
/// ```rust
/// # fn test() -> anyhow::Result<()> {
/// use ostree_ext::refescape;
/// let s = "registry:quay.io/coreos/fedora:latest";
/// assert_eq!(refescape::unprefix_unescape_ref("container", "container/registry_3A_quay_2E_io/coreos/fedora_3A_latest")?, s);
/// # Ok(())
/// # }
/// ```
pub fn unprefix_unescape_ref(prefix: &str, ostree_ref: &str) -> Result<String> {
let rest = ostree_ref
.strip_prefix(prefix)
.map(|s| s.strip_prefix('/'))
.flatten()
.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 TESTPREFIX: &str = "testprefix/blah";

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",
];
const CORNERCASES: &[&str] = &["/", "blah/", "/foo/"];

#[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).chain(CORNERCASES) {
let escaped = &prefix_escape_for_ref(TESTPREFIX, v).unwrap();
ostree::validate_rev(escaped).unwrap();
let unescaped = unprefix_unescape_ref(TESTPREFIX, &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 r = prefix_escape_for_ref(TESTPREFIX, &s);
let escaped = match r {
Ok(v) => v,
Err(_) => return TestResult::discard(),
};
let unescaped = unprefix_unescape_ref(TESTPREFIX, &escaped).unwrap();
TestResult::from_bool(unescaped == s)
}

#[test]
fn qcheck() {
quickcheck(roundtrip as fn(String) -> TestResult);
}
}

0 comments on commit 452215c

Please sign in to comment.