use crate::config::{FormatOptions, Layout, NameEncoding, RUNTIME_KEYS};
use crate::device::{BlockDevice, MemDevice};
use crate::error::{Error, Result};
use crate::fs::{Creator, Fs, SetAttr, ROOT_INO};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Demo {
pub size: u64,
pub options: FormatOptions,
pub label: String,
pub runtime: Vec<(String, String)>,
pub dirs: usize,
pub files: usize,
pub file_size: u64,
pub symlinks: usize,
pub hardlinks: usize,
pub user_links: usize,
pub delete_percent: u32,
pub compact: bool,
pub seed: u64,
}
impl Default for Demo {
fn default() -> Self {
Self {
size: 4 << 20,
options: FormatOptions::default(),
label: "demo".into(),
runtime: Vec::new(),
dirs: 3,
files: 12,
file_size: 12_000,
symlinks: 2,
hardlinks: 1,
user_links: 16,
delete_percent: 25,
compact: false,
seed: 1,
}
}
}
pub const DEMO_KEYS: &[(&str, &str)] = &[
("size", "image size in bytes (64K, 4M, ...)"),
("block-size", "block size: 1024 to 65536, a power of two"),
("layout", "united, split or data-only"),
("names", "name encoding: chars or packed"),
(
"journal-blocks",
"journal size in blocks (0: none, auto: from the size)",
),
("label", "volume label"),
("dirs", "directories under the root"),
("files", "regular files"),
("file-size", "largest file size in bytes"),
("symlinks", "symbolic links"),
("hardlinks", "extra hard links"),
("user-links", "user links in the embedded database"),
("delete-percent", "percentage of files deleted afterwards"),
("compact", "compact after deleting (true/false)"),
("seed", "seed of the pseudo-random workload"),
];
fn number<T: std::str::FromStr>(key: &str, value: &str) -> Result<T> {
value
.parse()
.map_err(|_| Error::invalid(format!("{key}: {value:?} is not a number")))
}
pub fn parse_size(text: &str) -> Result<u64> {
let text = text.trim();
let (digits, shift) = match text.chars().last().map(|c| c.to_ascii_uppercase()) {
Some('K') => (&text[..text.len() - 1], 10),
Some('M') => (&text[..text.len() - 1], 20),
Some('G') => (&text[..text.len() - 1], 30),
_ => (text, 0),
};
digits
.parse::<u64>()
.ok()
.and_then(|value| value.checked_mul(1 << shift))
.ok_or_else(|| Error::invalid(format!("bad size {text:?}")))
}
fn boolean(key: &str, value: &str) -> Result<bool> {
match value {
"true" | "on" | "yes" | "1" => Ok(true),
"false" | "off" | "no" | "0" => Ok(false),
_ => Err(Error::invalid(format!(
"{key}: {value:?} is not true or false"
))),
}
}
impl Demo {
pub fn parse(text: &str) -> Result<Self> {
let mut demo = Self::default();
for assignment in text.split(['\n', ';']) {
let assignment = assignment.trim();
if assignment.is_empty() || assignment.starts_with('#') {
continue;
}
let (key, value) = assignment
.split_once('=')
.ok_or_else(|| Error::invalid(format!("expected key=value, got {assignment:?}")))?;
demo.set(key.trim(), value.trim())?;
}
Ok(demo)
}
pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
match key {
"size" => self.size = parse_size(value)?,
"block-size" => self.options.block_size = number(key, value)?,
"layout" => self.options.layout = Layout::parse(value)?,
"names" => self.options.name_encoding = NameEncoding::parse(value)?,
"journal-blocks" => {
self.options.journal_blocks = if value == "auto" {
None
} else {
Some(number(key, value)?)
};
}
"label" => self.label = value.into(),
"dirs" => self.dirs = number(key, value)?,
"files" => self.files = number(key, value)?,
"file-size" => self.file_size = parse_size(value)?,
"symlinks" => self.symlinks = number(key, value)?,
"hardlinks" => self.hardlinks = number(key, value)?,
"user-links" => self.user_links = number(key, value)?,
"delete-percent" => self.delete_percent = number::<u32>(key, value)?.min(100),
"compact" => self.compact = boolean(key, value)?,
"seed" => self.seed = number(key, value)?,
_ if RUNTIME_KEYS.iter().any(|(name, _)| *name == key) => {
self.runtime.retain(|(name, _)| name != key);
self.runtime.push((key.into(), value.into()));
}
_ => return Err(Error::invalid(format!("unknown demo key {key:?}"))),
}
Ok(())
}
pub fn build(&self) -> Result<Fs<MemDevice>> {
let mut fs = Fs::format(MemDevice::new(self.size), &self.options, &self.label)?;
self.run(&mut fs)?;
Ok(fs)
}
pub fn run<D: BlockDevice>(&self, fs: &mut Fs<D>) -> Result<()> {
for (key, value) in &self.runtime {
fs.set_config_value(key, value)?;
}
let mut rng = Rng(self.seed.wrapping_mul(0x9e37_79b9_7f4a_7c15) | 1);
let creator = Creator::default();
let mut parents = vec![(ROOT_INO, String::new())];
for index in 0..self.dirs {
let name = format!("dir{index}");
let attr = fs.mkdir(ROOT_INO, &name, 0o755, creator)?;
parents.push((attr.ino, format!("/{name}")));
}
let mut files = Vec::new();
for index in 0..self.files {
let (parent, _) = parents[rng.below(parents.len() as u64) as usize].clone();
let name = format!("file{index}.bin");
let attr = fs.create(parent, &name, 0o644, creator)?;
let len = 1 + rng.below(self.file_size.max(1));
let data: Vec<u8> = (0..len)
.map(|at| (at as u8).wrapping_mul(31).wrapping_add(index as u8))
.collect();
fs.write(attr.ino, 0, &data)?;
fs.release(attr.ino)?;
files.push((parent, name, attr.ino));
}
for index in 0..self.symlinks {
let (parent, _) = parents[rng.below(parents.len() as u64) as usize].clone();
let target = files
.get(index % files.len().max(1))
.map_or_else(|| "nowhere".to_string(), |(_, name, _)| name.clone());
fs.symlink(parent, &format!("link{index}"), &target, creator)?;
}
for (index, (_, _, ino)) in files.iter().take(self.hardlinks).enumerate() {
fs.link(*ino, ROOT_INO, &format!("hard{index}"))?;
}
let mut previous = 0;
for index in 0..self.user_links {
previous = if index % 4 == 0 || previous == 0 {
fs.create_point()?
} else {
let source = previous;
let target = crate::links::external(rng.below(1000));
fs.create_link(source, target)?
};
}
let deletions = files.len() * self.delete_percent as usize / 100;
for _ in 0..deletions {
let (parent, name, _) = files.remove(rng.below(files.len() as u64) as usize);
fs.unlink(parent, &name)?;
}
if let Some((_, _, ino)) = files.first() {
let size = fs.getattr(*ino)?.size;
fs.setattr(
*ino,
SetAttr {
size: Some(size + 1),
..SetAttr::default()
},
)?;
}
if self.compact {
fs.compact()?;
}
fs.commit()
}
#[must_use]
pub fn to_text(&self) -> String {
let mut lines = vec![
format!("size={}", self.size),
format!("block-size={}", self.options.block_size),
format!("layout={}", self.options.layout.name()),
format!("names={}", self.options.name_encoding.name()),
format!(
"journal-blocks={}",
self.options
.journal_blocks
.map_or_else(|| "auto".into(), |blocks| blocks.to_string())
),
format!("label={}", self.label),
format!("dirs={}", self.dirs),
format!("files={}", self.files),
format!("file-size={}", self.file_size),
format!("symlinks={}", self.symlinks),
format!("hardlinks={}", self.hardlinks),
format!("user-links={}", self.user_links),
format!("delete-percent={}", self.delete_percent),
format!("compact={}", self.compact),
format!("seed={}", self.seed),
];
lines.extend(
self.runtime
.iter()
.map(|(key, value)| format!("{key}={value}")),
);
lines.join("\n")
}
}
struct Rng(u64);
impl Rng {
const fn next(&mut self) -> u64 {
self.0 ^= self.0 >> 12;
self.0 ^= self.0 << 25;
self.0 ^= self.0 >> 27;
self.0.wrapping_mul(0x2545_f491_4f6c_dd1d)
}
const fn below(&mut self, bound: u64) -> u64 {
if bound == 0 {
0
} else {
self.next() % bound
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parameters_round_trip_and_reject_unknown_keys() {
let demo =
Demo::parse("size=2M; block-size=1024\nlayout=split\nindex=adaptive\nfiles=5").unwrap();
assert_eq!(demo.size, 2 << 20);
assert_eq!(demo.options.block_size, 1024);
assert_eq!(demo.options.layout, Layout::Split);
assert_eq!(demo.files, 5);
assert_eq!(demo.runtime, vec![("index".into(), "adaptive".into())]);
assert_eq!(Demo::parse(&demo.to_text()).unwrap(), demo);
assert!(Demo::parse("colour=blue").is_err());
assert!(Demo::parse("files").is_err());
}
#[test]
fn the_same_parameters_build_the_same_layout() {
let demo = Demo::parse("size=1M; block-size=1024; files=8; user-links=9").unwrap();
let a = demo.build().unwrap();
let b = demo.build().unwrap();
assert_eq!(a.space_map(), b.space_map());
assert!(a.check().is_empty(), "{:?}", a.check());
assert_eq!(a.statfs().files, b.statfs().files);
}
}