Skip to content

Commit cb1970e

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

File tree

7 files changed

+676
-2
lines changed

7 files changed

+676
-2
lines changed

Cargo.toml

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,60 @@ rustdoc-args = ["--cfg", "docsrs"]
2222
[dependencies]
2323
align-address = "0.3"
2424
const_parse = "1"
25-
goblin = { version = "0.10", default-features = false, features = ["elf64"], optional = true }
2625
log = { version = "0.4", optional = true }
2726
plain = { version = "0.2", optional = true }
2827
time = { version = "0.3", default-features = false }
2928
uhyve-interface = "0.1"
3029

30+
[dependencies.byte-unit]
31+
version = "5"
32+
default-features = false
33+
features = ["byte", "serde"]
34+
optional = true
35+
36+
[dependencies.compression]
37+
version = "0.1"
38+
default-features = false
39+
features = ["gzip"]
40+
optional = true
41+
42+
[dependencies.goblin]
43+
version = "0.10"
44+
default-features = false
45+
features = ["elf64"]
46+
optional = true
47+
48+
[dependencies.num-traits]
49+
version = "0.2"
50+
default-features = false
51+
52+
[dependencies.serde]
53+
version = "1"
54+
default-features = false
55+
features = ["alloc", "derive"]
56+
optional = true
57+
58+
[dependencies.toml]
59+
version = "0.9"
60+
default-features = false
61+
features = ["parse", "serde"]
62+
optional = true
63+
64+
[dependencies.yoke]
65+
version = "0.8"
66+
default-features = false
67+
features = ["derive"]
68+
optional = true
69+
70+
[dev-dependencies.proptest]
71+
version = "1.9"
72+
3173
[features]
3274
default = []
33-
loader = ["log", "goblin", "plain"]
75+
std = ["alloc", "compression/std"]
76+
alloc = []
77+
compression = ["alloc", "dep:compression"]
78+
loader = ["dep:log", "dep:goblin", "dep:plain"]
3479
kernel = []
80+
config = ["alloc", "dep:byte-unit", "dep:serde", "dep:toml"]
81+
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+
The image itself is mapped (from the Hermit kernel perspective) into a path
41+
(`mount_point`) specified in the config file.

src/config.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#![allow(missing_docs)]
2+
3+
use alloc::string::String;
4+
use alloc::vec::Vec;
5+
6+
pub const DEFAULT_CONFIG_NAME: &str = "hermit.toml";
7+
pub type ParserError = toml::de::Error;
8+
9+
/// The image `hermit.toml` config file format.
10+
///
11+
/// All file paths are relative to the image root.
12+
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
13+
#[serde(tag = "version")]
14+
pub enum Config {
15+
#[serde(rename = "1")]
16+
V1 {
17+
input: Input,
18+
#[serde(default)]
19+
requirements: Requirements,
20+
21+
/// Kernel ELF file path
22+
kernel: String,
23+
24+
/// Mount point for the image (ROMfs)
25+
mount_point: String,
26+
},
27+
}
28+
29+
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
30+
pub struct Input {
31+
/// Arguments to be passed to the kernel
32+
pub kernel_args: Vec<String>,
33+
34+
/// Arguments to be passed to the application
35+
pub app_args: Vec<String>,
36+
37+
/// Environment variables
38+
pub env_vars: Vec<String>,
39+
}
40+
41+
/// Minimal requirements for an image to be able to run as expected
42+
#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
43+
pub struct Requirements {
44+
pub memory: Option<byte_unit::Byte>,
45+
46+
#[serde(default)]
47+
pub cpus: u32,
48+
}
49+
50+
#[inline]
51+
pub fn parse(data: &[u8]) -> Result<Config, ParserError> {
52+
toml::from_slice(data)
53+
}

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;
@@ -178,6 +192,20 @@ impl fmt::Display for UhyveIfVersion {
178192
}
179193
}
180194

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

0 commit comments

Comments
 (0)