forked from containers/bootc
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Colin Walters <[email protected]>
- Loading branch information
Showing
4 changed files
with
329 additions
and
1 deletion.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,321 @@ | ||
//! # Creating composefs images | ||
//! | ||
//! This code wraps `mkcomposefs` from the composefs project. | ||
use std::ffi::OsString; | ||
use std::fmt::Display; | ||
use std::fmt::Write as WriteFmt; | ||
use std::io::Write; | ||
use std::os::unix::ffi::{OsStrExt, OsStringExt}; | ||
use std::path::{Path, PathBuf}; | ||
use std::str::FromStr; | ||
|
||
use anyhow::{anyhow, Context, Result}; | ||
|
||
struct Xattr { | ||
key: OsString, | ||
value: Vec<u8>, | ||
} | ||
type Xattrs = Vec<Xattr>; | ||
|
||
struct Mtime { | ||
sec: u64, | ||
nsec: u64, | ||
} | ||
|
||
struct Entry { | ||
path: PathBuf, | ||
uid: u32, | ||
gid: u32, | ||
mode: u32, | ||
mtime: Mtime, | ||
item: Item, | ||
xattrs: Xattrs, | ||
} | ||
|
||
enum Item { | ||
Regular { | ||
size: u64, | ||
nlink: u32, | ||
inline_content: Option<Vec<u8>>, | ||
fsverity_digest: String, | ||
}, | ||
Device { | ||
nlink: u32, | ||
rdev: u32, | ||
}, | ||
Symlink { | ||
nlink: u32, | ||
target: PathBuf, | ||
}, | ||
Hardlink { | ||
target: PathBuf, | ||
}, | ||
Directory {}, | ||
} | ||
|
||
fn unescape(s: &str) -> Result<Vec<u8>> { | ||
let mut it = s.chars(); | ||
let mut r = Vec::new(); | ||
while let Some(c) = it.next() { | ||
if c != '\\' { | ||
write!(r, "{c}").unwrap(); | ||
continue; | ||
} | ||
let c = it.next().ok_or_else(|| anyhow!("Unterminated escape"))?; | ||
let c = match c { | ||
'\\' => '\\' as u8, | ||
'n' => '\n' as u8, | ||
'r' => '\r' as u8, | ||
't' => '\t' as u8, | ||
'x' => { | ||
let mut s = String::new(); | ||
s.push( | ||
it.next() | ||
.ok_or_else(|| anyhow!("Unterminated hex escape"))?, | ||
); | ||
s.push( | ||
it.next() | ||
.ok_or_else(|| anyhow!("Unterminated hex escape"))?, | ||
); | ||
let v = u8::from_str_radix(&s, 16)?; | ||
v | ||
} | ||
o => anyhow::bail!("Invalid escape {o}"), | ||
}; | ||
} | ||
Ok(r) | ||
} | ||
|
||
fn escape(s: &[u8]) -> String { | ||
let mut r = String::new(); | ||
for c in s.iter().copied() { | ||
if c != b'\\' && (c.is_ascii_alphanumeric() || c.is_ascii_punctuation()) { | ||
r.push(c as char); | ||
} else { | ||
match c { | ||
b'\\' => r.push_str(r"\\"), | ||
b'\n' => r.push_str(r"\n"), | ||
b'\t' => r.push_str(r"\t"), | ||
o => { | ||
write!(r, "\\x{:x}", o).unwrap(); | ||
} | ||
} | ||
} | ||
} | ||
r | ||
} | ||
|
||
fn optional_str(s: &str) -> Option<&str> { | ||
match s { | ||
"-" => None, | ||
o => Some(o), | ||
} | ||
} | ||
|
||
impl FromStr for Mtime { | ||
type Err = anyhow::Error; | ||
|
||
fn from_str(s: &str) -> Result<Self> { | ||
let (sec, nsec) = s | ||
.split_once('.') | ||
.ok_or_else(|| anyhow!("Missing . in mtime"))?; | ||
Ok(Self { | ||
sec: u64::from_str(sec)?, | ||
nsec: u64::from_str(nsec)?, | ||
}) | ||
} | ||
} | ||
|
||
impl FromStr for Xattr { | ||
type Err = anyhow::Error; | ||
|
||
fn from_str(s: &str) -> Result<Self> { | ||
let (key, value) = s | ||
.split_once('=') | ||
.ok_or_else(|| anyhow!("Missing = in xattrs"))?; | ||
let key = OsString::from_vec(unescape(key)?); | ||
let value = unescape(value)?; | ||
Ok(Self { key, value }) | ||
} | ||
} | ||
|
||
impl std::str::FromStr for Entry { | ||
type Err = anyhow::Error; | ||
|
||
fn from_str(s: &str) -> Result<Self> { | ||
let mut components = s.split(' '); | ||
let mut next = |name: &str| components.next().ok_or_else(|| anyhow!("Missing {name}")); | ||
let path = next("path")?; | ||
let path: PathBuf = OsString::from_vec(unescape(path)?).into(); | ||
let size = u64::from_str(next("size")?)?; | ||
let modeval = next("mode")?; | ||
let (is_hardlink, mode) = if let Some((_, rest)) = modeval.split_once('@') { | ||
(true, u32::from_str_radix(rest, 8)?) | ||
} else { | ||
(false, u32::from_str_radix(modeval, 8)?) | ||
}; | ||
let nlink = u32::from_str(next("nlink")?)?; | ||
let uid = u32::from_str(next("uid")?)?; | ||
let gid = u32::from_str(next("gid")?)?; | ||
let rdev = u32::from_str(next("rdev")?)?; | ||
let mtime = Mtime::from_str(next("mtime")?)?; | ||
let payload = optional_str(next("payload")?); | ||
let content = optional_str(next("content")?); | ||
let digest = optional_str(next("digest")?); | ||
let xattrs = components | ||
.map(Xattr::from_str) | ||
.collect::<Result<Vec<_>>>()?; | ||
|
||
let item = if is_hardlink { | ||
let target = OsString::from_vec(unescape( | ||
payload.ok_or_else(|| anyhow!("Missing payload"))?, | ||
)?) | ||
.into(); | ||
Item::Hardlink { target } | ||
} else { | ||
match libc::S_IFMT & mode { | ||
libc::S_IFREG => { | ||
let fsverity_digest = | ||
digest.ok_or_else(|| anyhow!("Missing digest"))?.to_owned(); | ||
Item::Regular { | ||
size, | ||
nlink, | ||
inline_content: content.map(unescape).transpose()?, | ||
fsverity_digest, | ||
} | ||
} | ||
libc::S_IFLNK => { | ||
let target = OsString::from_vec(unescape( | ||
payload.ok_or_else(|| anyhow!("Missing payload"))?, | ||
)?) | ||
.into(); | ||
Item::Symlink { nlink, target } | ||
} | ||
libc::S_IFCHR | libc::S_IFBLK => Item::Device { nlink, rdev }, | ||
libc::S_IFDIR => Item::Directory {}, | ||
o => { | ||
anyhow::bail!("Unhandled mode {o:o}") | ||
} | ||
} | ||
}; | ||
Ok(Entry { | ||
path, | ||
uid, | ||
gid, | ||
mode, | ||
mtime, | ||
item, | ||
xattrs, | ||
}) | ||
} | ||
} | ||
|
||
impl Item { | ||
pub(crate) fn size(&self) -> u64 { | ||
match self { | ||
Item::Regular { size, .. } => *size, | ||
o => 0, | ||
} | ||
} | ||
|
||
pub(crate) fn nlink(&self) -> u32 { | ||
match self { | ||
Item::Regular { nlink, .. } => *nlink, | ||
Item::Device { nlink, .. } => *nlink, | ||
Item::Symlink { nlink, .. } => *nlink, | ||
_ => 0, | ||
} | ||
} | ||
|
||
pub(crate) fn rdev(&self) -> u32 { | ||
match self { | ||
Item::Device { rdev, .. } => *rdev, | ||
_ => 0, | ||
} | ||
} | ||
|
||
pub(crate) fn payload(&self) -> Option<&Path> { | ||
match self { | ||
Item::Symlink { target, .. } => Some(target), | ||
Item::Hardlink { target } => Some(target), | ||
_ => None, | ||
} | ||
} | ||
|
||
pub(crate) fn content(&self) -> Option<&[u8]> { | ||
match self { | ||
Item::Regular { inline_content, .. } => inline_content.as_deref(), | ||
_ => None, | ||
} | ||
} | ||
} | ||
|
||
impl Display for Mtime { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
write!(f, "{}.{}", self.sec, self.nsec) | ||
} | ||
} | ||
|
||
impl Display for Entry { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
write!(f, "{}", escape(self.path.as_os_str().as_bytes()))?; | ||
write!( | ||
f, | ||
" {} {:o} {} {} {} {} {} ", | ||
self.item.size(), | ||
self.mode, | ||
self.item.nlink(), | ||
self.uid, | ||
self.gid, | ||
self.item.rdev(), | ||
self.mtime, | ||
)?; | ||
if let Some(payload) = self.item.payload() { | ||
write!(f, "{payload:?} ")?; | ||
} else { | ||
write!(f, "- ")?; | ||
} | ||
match &self.item { | ||
Item::Regular { | ||
fsverity_digest, | ||
inline_content, | ||
.. | ||
} => { | ||
if let Some(content) = inline_content { | ||
let escaped = escape(&content); | ||
write!(f, "{escaped} ")?; | ||
} else { | ||
write!(f, "-")?; | ||
} | ||
write!(f, "{fsverity_digest} ")?; | ||
} | ||
_ => { | ||
write!(f, "- -")?; | ||
} | ||
} | ||
for xattr in self.xattrs.iter() { | ||
write!( | ||
f, | ||
" {}={}", | ||
escape(xattr.key.as_bytes()), | ||
escape(&xattr.value) | ||
)?; | ||
} | ||
std::fmt::Result::Ok(()) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_parse() { | ||
const CONTENT: &str = include_str!("fixtures/composefs-example.txt"); | ||
for line in CONTENT.lines() { | ||
let e = Entry::from_str(line).unwrap(); | ||
println!("{e}"); | ||
} | ||
} | ||
} |
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,6 @@ | ||
/ 4096 40755 4 1000 1000 0 1695372970.944925700 ‐ ‐ ‐ security.selinux=unconfined_u:object_r:unlabeled_t:s0\x00 | ||
/a\x20dir\x20w\x20space 27 40755 2 1000 1000 0 1694598852.869646118 ‐ ‐ ‐ security.selinux=unconfined_u:object_r:unlabeled_t:s0\x00 | ||
/a‐dir 45 40755 2 1000 1000 0 1674041780.601887980 ‐ ‐ ‐ security.selinux=unconfined_u:object_r:unlabeled_t:s0\x00 | ||
/a‐dir/a‐file 259 100644 1 1000 1000 0 1695368732.385062094 35/d02f81325122d77ec1d11baba655bc9bf8a891ab26119a41c50fa03ddfb408 ‐ 35d02f81325122d77ec1d11baba655bc9bf8a891ab26119a41c50fa03ddfb408 security.selinux=unconfined_u:object_r:unlabeled_t:s0\x00 | ||
/a‐hardlink 259 @100644 1 1000 1000 0 1695368732.385062094 /a‐dir/a‐file ‐ 35d02f81325122d77ec1d11baba655bc9bf8a891ab26119a41c50fa03ddfb408 security.selinux=unconfined_u:object_r:unlabeled_t:s0\x00 | ||
/inline.txt 10 100644 1 1000 1000 0 1697019909.446146440 ‐ some‐text\n ‐ security.selinux=unconfined_u:object_r:unlabeled_t:s0\x00 |
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