Skip to content

Commit c55f6bb

Browse files
committed
feat: Merge hermit-image-reader into this crate
Signed-off-by: Ellen Εμιλία Άννα Zscheile <[email protected]>
1 parent 0492922 commit c55f6bb

File tree

7 files changed

+655
-1
lines changed

7 files changed

+655
-1
lines changed

Cargo.toml

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,43 @@ rustdoc-args = ["--cfg", "docsrs"]
2121

2222
[dependencies]
2323
align-address = "0.3"
24+
compression = { version = "0.1", default-features = false, features = ["gzip"], optional = true }
2425
const_parse = "1"
2526
goblin = { version = "0.10", default-features = false, features = ["elf64"], optional = true }
2627
log = { version = "0.4", optional = true }
28+
num-traits = { version = "0.2", default-features = false }
2729
plain = { version = "0.2", optional = true }
2830
time = { version = "0.3", default-features = false }
2931
uhyve-interface = "0.1"
32+
yoke = { version = "0.8", default-features = false, features = ["derive"], optional = true }
33+
34+
[dependencies.byte-unit]
35+
version = "5"
36+
default-features = false
37+
features = ["byte", "serde"]
38+
optional = true
39+
40+
[dependencies.serde]
41+
version = "1"
42+
default-features = false
43+
features = ["alloc", "derive"]
44+
optional = true
45+
46+
[dependencies.toml]
47+
version = "0.9"
48+
default-features = false
49+
features = ["parse", "serde"]
50+
optional = true
51+
52+
[dev-dependencies]
53+
proptest = "1.9"
3054

3155
[features]
3256
default = []
33-
loader = ["log", "goblin", "plain"]
57+
std = ["alloc", "compression/std"]
58+
alloc = []
59+
compression = ["alloc", "dep:compression"]
60+
loader = ["dep:log", "dep:goblin", "dep:plain"]
3461
kernel = []
62+
config = ["alloc", "dep:byte-unit", "dep:serde", "dep:toml"]
63+
thin-tree = ["alloc", "dep:yoke"]

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,20 @@ at your option.
2222
Unless you explicitly state otherwise, any contribution intentionally submitted
2323
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
2424
dual licensed as above, without any additional terms or conditions.
25+
26+
## Hermit images
27+
28+
This Rust crate also implements a basic reader for Hermit images.
29+
Overall, these are just `.tar.gz` (i.e. gzipped tar) files.
30+
31+
They contain at least 2 special entries:
32+
* The config file (in TOML format), at `hermit.toml` in the image root.
33+
The expected entries are described in the crate documentation in `hermit_entry::config::Config` (requires enabling the `config` feature).
34+
* A Hermit Kernel ELF file, whose path is specified in the config.
35+
36+
For performance reasons, it should be preferred to put the config and kernel
37+
as the first two entries of the image (tar files don't have any sorting or index,
38+
except that normally, the latest entry of the file takes precedence).
39+
40+
All subdirectories of the image itself are mapped
41+
(from the Hermit kernel perspective) into `/`.

src/config.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! The image `hermit.toml` config file format.
2+
//!
3+
//! All file paths are relative to the image root.
4+
//!
5+
//! Note that for technical reasons, files in the top-level of the image
6+
//! might not be reachable from the kernel, but root subdirectories should be,
7+
//! unless they are named after reserved names like `proc`.
8+
9+
use alloc::string::String;
10+
use alloc::vec::Vec;
11+
12+
/// The default configuration file name, relative to the image root.
13+
pub const DEFAULT_CONFIG_NAME: &str = "hermit.toml";
14+
15+
/// The possible errors which the parser might emit.
16+
pub type ParserError = toml::de::Error;
17+
18+
/// The configuration toplevl structure.
19+
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
20+
#[serde(tag = "version")]
21+
pub enum Config {
22+
/// The first (and current) version of the config format.
23+
#[serde(rename = "1")]
24+
V1 {
25+
/// Input parameter for the kernel and application
26+
input: Input,
27+
28+
/// Minimal requirements for an image to be able to run as expected
29+
#[serde(default)]
30+
requirements: Requirements,
31+
32+
/// Kernel ELF file path
33+
kernel: String,
34+
},
35+
}
36+
37+
/// Input parameter for the kernel and application
38+
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
39+
pub struct Input {
40+
/// Arguments to be passed to the kernel
41+
pub kernel_args: Vec<String>,
42+
43+
/// Arguments to be passed to the application
44+
pub app_args: Vec<String>,
45+
46+
/// Environment variables
47+
pub env_vars: Vec<String>,
48+
}
49+
50+
/// Minimal requirements for an image to be able to run as expected
51+
#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
52+
pub struct Requirements {
53+
/// Minimum RAM
54+
pub memory: Option<byte_unit::Byte>,
55+
56+
/// Minimum amount of CPUs
57+
#[serde(default)]
58+
pub cpus: u32,
59+
}
60+
61+
/// Parse a config file from a byte slice.
62+
#[inline]
63+
pub fn parse(data: &[u8]) -> Result<Config, ParserError> {
64+
toml::from_slice(data)
65+
}

src/filename.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
use core::{cmp, fmt};
2+
3+
const SEP: u8 = b'/';
4+
5+
/// Truncate a byte slice to the first NULL byte. if any.
6+
// taken from `tar-rs`
7+
pub fn truncate(slice: &[u8]) -> &[u8] {
8+
match slice.iter().position(|i| *i == 0) {
9+
Some(i) => &slice[..i],
10+
None => slice,
11+
}
12+
}
13+
14+
// FIXME: once `slice`.split_once is stable, we can get rid of this.
15+
fn split_once(this: &[u8], on: u8) -> Option<(&[u8], &[u8])> {
16+
let index = this.iter().position(|i| *i == on)?;
17+
Some((&this[..index], &this[index + 1..]))
18+
}
19+
20+
/// Zero-copy file name (usually from an in-memory tar file)
21+
///
22+
/// Code might rely on the invariant that it doesn't contain null bytes.
23+
#[derive(Clone, Copy, Debug, Eq)]
24+
pub enum Filename<'a> {
25+
/// A simple file name
26+
One(&'a [u8]),
27+
/// A prefix and file name (meaning `{0}/{1}`)
28+
Two(&'a [u8], &'a [u8]),
29+
}
30+
31+
impl Filename<'_> {
32+
/// Truncate the filename to the first `n` components
33+
#[must_use = "input is not modified"]
34+
pub fn as_truncated(self, mut n: usize) -> Self {
35+
let handle_parts = |n: &mut usize, x: &mut &[u8]| {
36+
if *n > 0 {
37+
let mut offset = 0;
38+
for i in (*x).split(|i| *i == SEP) {
39+
*n -= 1;
40+
offset += i.len();
41+
if *n == 0 {
42+
// truncate
43+
*x = &x[..offset];
44+
break;
45+
}
46+
// separator
47+
offset += 1;
48+
}
49+
}
50+
};
51+
52+
match self {
53+
Self::One(_) if n == 0 => Self::One(&[]),
54+
Self::One(mut x) => {
55+
handle_parts(&mut n, &mut x);
56+
Self::One(x)
57+
}
58+
Self::Two(mut x, mut y) => {
59+
handle_parts(&mut n, &mut x);
60+
if n >= 1 {
61+
handle_parts(&mut n, &mut y);
62+
Self::Two(x, y)
63+
} else {
64+
Self::One(x)
65+
}
66+
}
67+
}
68+
}
69+
}
70+
71+
impl fmt::Display for Filename<'_> {
72+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73+
match self {
74+
Self::One(x) => write!(f, "{:?}", x),
75+
Self::Two(x, y) => write!(f, "{:?}/{:?}", x, y),
76+
}
77+
}
78+
}
79+
80+
impl cmp::PartialEq for Filename<'_> {
81+
fn eq(&self, oth: &Self) -> bool {
82+
let (mut this, mut oth) = (*self, *oth);
83+
loop {
84+
match (this.next(), oth.next()) {
85+
(None, None) => break true,
86+
(None, _) | (_, None) => break false,
87+
(Some(x), Some(y)) if x != y => break false,
88+
_ => {}
89+
}
90+
}
91+
}
92+
}
93+
94+
impl<'a> From<&'a [u8]> for Filename<'a> {
95+
#[inline]
96+
fn from(x: &'a [u8]) -> Self {
97+
// null bytes are the only illegal bytes in a filename
98+
assert!(!x.contains(&0x00));
99+
Self::One(x)
100+
}
101+
}
102+
103+
impl<'a> From<&'a str> for Filename<'a> {
104+
#[inline]
105+
fn from(x: &'a str) -> Self {
106+
// null bytes are the only illegal bytes in a str filename
107+
assert!(!x.contains('\0'));
108+
Self::One(x.as_bytes())
109+
}
110+
}
111+
112+
impl<'a> Iterator for Filename<'a> {
113+
type Item = &'a [u8];
114+
115+
fn next(&mut self) -> Option<&'a [u8]> {
116+
match self {
117+
Self::One([]) => None,
118+
Self::One(x) => Some(match split_once(x, SEP) {
119+
None => {
120+
let ret = *x;
121+
*x = &[];
122+
ret
123+
}
124+
Some((fi, rest)) => {
125+
*x = rest;
126+
fi
127+
}
128+
}),
129+
Self::Two(x, y) => Some(match split_once(x, SEP) {
130+
None => {
131+
let ret = *x;
132+
*self = Self::One(y);
133+
ret
134+
}
135+
Some((fi, rest)) => {
136+
*x = rest;
137+
fi
138+
}
139+
}),
140+
}
141+
}
142+
}
143+
144+
#[cfg(test)]
145+
mod tests {
146+
use super::*;
147+
148+
fn assert_iteq<'a, I: Iterator<Item = &'a [u8]>>(mut it: I, eqto: &[&str]) {
149+
for i in eqto {
150+
assert_eq!(it.next(), Some(i.as_bytes()), "divergence @ {}", i);
151+
}
152+
assert_eq!(it.next(), None);
153+
}
154+
155+
#[test]
156+
fn test_filename_iter_one() {
157+
let it = Filename::One(b"aleph/beta/omicron");
158+
assert_iteq(it, &["aleph", "beta", "omicron"]);
159+
}
160+
161+
#[test]
162+
fn test_filename_as_truncated_one() {
163+
let it = Filename::One(b"aleph/beta/omicron").as_truncated(2);
164+
assert_iteq(it, &["aleph", "beta"]);
165+
}
166+
167+
#[test]
168+
fn test_filename_iter_two() {
169+
let it = Filename::Two(b"aleph/beta", b"omicron/depth");
170+
assert_iteq(it, &["aleph", "beta", "omicron", "depth"]);
171+
}
172+
173+
#[test]
174+
fn test_filename_as_truncated_two() {
175+
let mfn = Filename::Two(b"aleph/beta", b"omicron/depth");
176+
assert_iteq(mfn.as_truncated(2), &["aleph", "beta"]);
177+
assert_iteq(mfn.as_truncated(3), &["aleph", "beta", "omicron"]);
178+
assert_iteq(mfn.as_truncated(4), &["aleph", "beta", "omicron", "depth"]);
179+
assert_iteq(mfn.as_truncated(5), &["aleph", "beta", "omicron", "depth"]);
180+
}
181+
182+
#[test]
183+
fn test_filename_non_ascii() {
184+
let mfn = Filename::Two("αleph/βetα".as_bytes(), "ο/δth".as_bytes());
185+
assert_iteq(mfn, &["αleph", "βetα", "ο", "δth"]);
186+
assert_iteq(mfn.as_truncated(2), &["αleph", "βetα"]);
187+
assert_iteq(mfn.as_truncated(3), &["αleph", "βetα", "ο"]);
188+
assert_iteq(mfn.as_truncated(4), &["αleph", "βetα", "ο", "δth"]);
189+
assert_iteq(mfn.as_truncated(5), &["αleph", "βetα", "ο", "δth"]);
190+
}
191+
}

src/lib.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,28 @@
88
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
99
#![warn(missing_docs)]
1010

11+
#[cfg(feature = "alloc")]
12+
extern crate alloc;
13+
1114
pub mod boot_info;
1215

16+
#[cfg(feature = "config")]
17+
pub mod config;
18+
1319
#[cfg(feature = "loader")]
1420
pub mod elf;
1521

22+
mod filename;
23+
pub use filename::Filename;
24+
1625
#[cfg(feature = "kernel")]
1726
mod note;
1827

28+
pub mod tar_parser;
29+
30+
#[cfg(feature = "thin-tree")]
31+
pub mod thin_tree;
32+
1933
use core::error::Error;
2034
use core::fmt;
2135
use core::str::FromStr;
@@ -179,6 +193,20 @@ impl fmt::Display for UhyveIfVersion {
179193
}
180194
}
181195

196+
#[cfg(feature = "compression")]
197+
/// We assume that all images are gzip-compressed
198+
pub fn decompress_image(
199+
data: &[u8],
200+
) -> Result<tar_parser::BytesBuf, compression::prelude::CompressionError> {
201+
use compression::prelude::{DecodeExt as _, GZipDecoder};
202+
203+
data.iter()
204+
.copied()
205+
.decode(&mut GZipDecoder::new())
206+
.collect::<Result<_, _>>()
207+
.map(tar_parser::BytesBuf)
208+
}
209+
182210
#[cfg(test)]
183211
mod tests {
184212
use super::*;

0 commit comments

Comments
 (0)