use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;
use core::ops::RangeInclusive;
use crate::core::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Duration {
picos: u64,
}
impl Duration {
pub const ZERO: Duration = Duration { picos: 0 };
pub const MAX: Duration = Duration { picos: u64::MAX };
#[inline]
pub const fn from_picos(picos: u64) -> Duration {
Duration { picos }
}
pub const fn from_nanos(nanos: u64) -> Option<Duration> {
match nanos.checked_mul(1_000) {
Some(picos) => Some(Duration { picos }),
None => None,
}
}
pub const fn from_micros(micros: u64) -> Option<Duration> {
match micros.checked_mul(1_000_000) {
Some(picos) => Some(Duration { picos }),
None => None,
}
}
pub const fn from_millis(millis: u64) -> Option<Duration> {
match millis.checked_mul(1_000_000_000) {
Some(picos) => Some(Duration { picos }),
None => None,
}
}
pub const fn from_secs(secs: u64) -> Option<Duration> {
match secs.checked_mul(1_000_000_000_000) {
Some(picos) => Some(Duration { picos }),
None => None,
}
}
#[inline]
pub const fn as_picos(self) -> u64 {
self.picos
}
#[inline]
pub const fn as_nanos(self) -> u64 {
self.picos / 1_000
}
#[inline]
pub const fn as_micros(self) -> u64 {
self.picos / 1_000_000
}
#[inline]
pub const fn as_millis(self) -> u64 {
self.picos / 1_000_000_000
}
#[inline]
pub const fn as_secs(self) -> u64 {
self.picos / 1_000_000_000_000
}
}
impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const UNITS: [(u64, &str); 7] = [
(3_600_000_000_000_000, "h"),
(60_000_000_000_000, "m"),
(1_000_000_000_000, "s"),
(1_000_000_000, "ms"),
(1_000_000, "us"),
(1_000, "ns"),
(1, "ps"),
];
if self.picos == 0 {
return f.write_str("0s");
}
for (scale, name) in UNITS {
if self.picos.is_multiple_of(scale) {
return write!(f, "{}{}", self.picos / scale, name);
}
}
write!(f, "{}ps", self.picos)
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Media {
name: String,
bytes: Arc<[u8]>,
}
impl Media {
pub fn new(name: impl Into<String>, bytes: impl Into<Arc<[u8]>>) -> Media {
Media {
name: name.into(),
bytes: bytes.into(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn to_bytes(&self) -> Arc<[u8]> {
Arc::clone(&self.bytes)
}
pub fn len(&self) -> u64 {
self.bytes.len() as u64
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
}
impl fmt::Debug for Media {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Media")
.field("name", &self.name)
.field("len", &self.bytes.len())
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Link(String);
impl Link {
pub fn new(path: impl Into<String>) -> Result<Link> {
let path = path.into();
if path.is_empty() {
return Err(prop_err("a link path cannot be empty".to_owned()));
}
for segment in path.split('.') {
if segment.is_empty() {
return Err(prop_err(format!(
"`{path}` is not a valid link path: empty path segment"
)));
}
if let Some(bad) = segment
.chars()
.find(|c| !c.is_ascii_alphanumeric() && *c != '_' && *c != '-')
{
return Err(prop_err(format!(
"`{path}` is not a valid link path: unexpected `{bad}` \
(segments are alphanumerics, `_` or `-`, separated by `.`)"
)));
}
}
Ok(Link(path))
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
pub fn segments(&self) -> impl Iterator<Item = &str> {
self.0.split('.')
}
pub fn root(&self) -> &str {
self.0.split('.').next().unwrap_or(&self.0)
}
}
impl fmt::Display for Link {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ValueKind {
Int,
Uint,
Bool,
Str,
Size,
Addr,
Duration,
List,
Map,
Link,
Media,
}
impl ValueKind {
pub const fn as_str(self) -> &'static str {
match self {
ValueKind::Int => "signed integer",
ValueKind::Uint => "unsigned integer",
ValueKind::Bool => "boolean",
ValueKind::Str => "string",
ValueKind::Size => "size",
ValueKind::Addr => "address",
ValueKind::Duration => "duration",
ValueKind::List => "list",
ValueKind::Map => "map",
ValueKind::Link => "link",
ValueKind::Media => "media",
}
}
}
impl fmt::Display for ValueKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Value {
Int(i64),
Uint(u64),
Bool(bool),
Str(String),
Size(u64),
Addr(u64),
Duration(Duration),
List(Vec<Value>),
Map(Props),
Link(Link),
Media(Media),
}
impl Value {
pub fn kind(&self) -> ValueKind {
match self {
Value::Int(_) => ValueKind::Int,
Value::Uint(_) => ValueKind::Uint,
Value::Bool(_) => ValueKind::Bool,
Value::Str(_) => ValueKind::Str,
Value::Size(_) => ValueKind::Size,
Value::Addr(_) => ValueKind::Addr,
Value::Duration(_) => ValueKind::Duration,
Value::List(_) => ValueKind::List,
Value::Map(_) => ValueKind::Map,
Value::Link(_) => ValueKind::Link,
Value::Media(_) => ValueKind::Media,
}
}
pub fn is_numeric(&self) -> bool {
matches!(
self,
Value::Int(_) | Value::Uint(_) | Value::Size(_) | Value::Addr(_)
)
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Value::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_int(&self) -> Option<i64> {
match self {
Value::Int(i) => Some(*i),
Value::Uint(u) | Value::Size(u) | Value::Addr(u) => i64::try_from(*u).ok(),
_ => None,
}
}
pub fn as_uint(&self) -> Option<u64> {
match self {
Value::Uint(u) | Value::Size(u) | Value::Addr(u) => Some(*u),
Value::Int(i) => u64::try_from(*i).ok(),
_ => None,
}
}
pub fn as_size(&self) -> Option<u64> {
self.as_uint()
}
pub fn as_addr(&self) -> Option<u64> {
self.as_uint()
}
pub fn as_str(&self) -> Option<&str> {
match self {
Value::Str(s) => Some(s.as_str()),
_ => None,
}
}
pub fn as_duration(&self) -> Option<Duration> {
match self {
Value::Duration(d) => Some(*d),
_ => None,
}
}
pub fn as_list(&self) -> Option<&[Value]> {
match self {
Value::List(items) => Some(items.as_slice()),
_ => None,
}
}
pub fn as_map(&self) -> Option<&Props> {
match self {
Value::Map(m) => Some(m),
_ => None,
}
}
pub fn as_link(&self) -> Option<&Link> {
match self {
Value::Link(l) => Some(l),
_ => None,
}
}
pub fn as_media(&self) -> Option<&Media> {
match self {
Value::Media(m) => Some(m),
_ => None,
}
}
pub fn to_bool(&self, prop: &str) -> Result<bool> {
match self {
Value::Bool(b) => Ok(*b),
Value::Str(s) if matches!(s.as_str(), "true" | "false") => Err(type_error_hint(
prop,
ValueKind::Bool,
self,
"booleans are written without quotes",
)),
_ => Err(type_error(prop, ValueKind::Bool, self)),
}
}
pub fn to_int(&self, prop: &str) -> Result<i64> {
match self.as_int() {
Some(i) => Ok(i),
None if self.is_numeric() => Err(prop_err(format!(
"property `{prop}`: {self} does not fit in a signed 64-bit integer"
))),
None => Err(type_error(prop, ValueKind::Int, self)),
}
}
pub fn to_uint(&self, prop: &str) -> Result<u64> {
self.to_unsigned(prop, ValueKind::Uint)
}
pub fn to_size(&self, prop: &str) -> Result<u64> {
self.to_unsigned(prop, ValueKind::Size)
}
pub fn to_addr(&self, prop: &str) -> Result<u64> {
self.to_unsigned(prop, ValueKind::Addr)
}
fn to_unsigned(&self, prop: &str, want: ValueKind) -> Result<u64> {
match self.as_uint() {
Some(u) => Ok(u),
None if self.is_numeric() => Err(prop_err(format!(
"property `{prop}`: expected {want}, found the negative value {self}"
))),
None => Err(type_error(prop, want, self)),
}
}
pub fn to_duration(&self, prop: &str) -> Result<Duration> {
match self {
Value::Duration(d) => Ok(*d),
_ if self.is_numeric() => Err(type_error_hint(
prop,
ValueKind::Duration,
self,
"durations need a unit, as in `10ms`",
)),
_ => Err(type_error(prop, ValueKind::Duration, self)),
}
}
pub fn to_str(&self, prop: &str) -> Result<&str> {
self.as_str()
.ok_or_else(|| type_error(prop, ValueKind::Str, self))
}
pub fn to_list(&self, prop: &str) -> Result<&[Value]> {
self.as_list()
.ok_or_else(|| type_error(prop, ValueKind::List, self))
}
pub fn to_map(&self, prop: &str) -> Result<&Props> {
self.as_map()
.ok_or_else(|| type_error(prop, ValueKind::Map, self))
}
pub fn to_link(&self, prop: &str) -> Result<&Link> {
self.as_link()
.ok_or_else(|| type_error(prop, ValueKind::Link, self))
}
pub fn to_media(&self, prop: &str) -> Result<&Media> {
match self {
Value::Media(m) => Ok(m),
Value::Str(slot) => Err(type_error_hint(
prop,
ValueKind::Media,
self,
&format!("nothing is bound to the media slot `{slot}`"),
)),
_ => Err(type_error(prop, ValueKind::Media, self)),
}
}
pub fn parse_scalar(text: &str) -> Value {
let s = text.trim();
if let Ok(b) = parse_bool(s) {
return Value::Bool(b);
}
if let Ok(u) = parse_uint(s) {
return Value::Uint(u);
}
if let Ok(i) = parse_int(s) {
return Value::Int(i);
}
if let Ok(n) = parse_size(s) {
return Value::Size(n);
}
if let Ok(d) = parse_duration(s) {
return Value::Duration(d);
}
Value::Str(s.to_owned())
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::Int(i) => write!(f, "{i}"),
Value::Uint(u) => write!(f, "{u}"),
Value::Bool(b) => write!(f, "{b}"),
Value::Str(s) => write!(f, "\"{s}\""),
Value::Size(n) => f.write_str(&format_size(*n)),
Value::Addr(a) => write!(f, "{a:#x}"),
Value::Duration(d) => write!(f, "{d}"),
Value::List(items) => {
f.write_str("[")?;
for (i, item) in items.iter().enumerate() {
if i != 0 {
f.write_str(", ")?;
}
write!(f, "{item}")?;
}
f.write_str("]")
}
Value::Map(m) => {
f.write_str("{")?;
for (i, (name, value)) in m.iter().enumerate() {
if i != 0 {
f.write_str(",")?;
}
write!(f, " {name} = {value}")?;
}
f.write_str(" }")
}
Value::Link(l) => write!(f, "{l}"),
Value::Media(m) => write!(f, "media `{}` ({} bytes)", m.name(), m.len()),
}
}
}
fn format_size(n: u64) -> String {
const SUFFIXES: [&str; 6] = ["K", "M", "G", "T", "P", "E"];
if n != 0 {
for (i, suffix) in SUFFIXES.iter().enumerate().rev() {
let scale = 1u64 << (10 * (i as u32 + 1));
if n.is_multiple_of(scale) {
return format!("{}{}", n / scale, suffix);
}
}
}
format!("{n}")
}
impl From<bool> for Value {
fn from(b: bool) -> Value {
Value::Bool(b)
}
}
impl From<i64> for Value {
fn from(i: i64) -> Value {
Value::Int(i)
}
}
impl From<u64> for Value {
fn from(u: u64) -> Value {
Value::Uint(u)
}
}
impl From<u32> for Value {
fn from(u: u32) -> Value {
Value::Uint(u as u64)
}
}
impl From<&str> for Value {
fn from(s: &str) -> Value {
Value::Str(s.to_owned())
}
}
impl From<String> for Value {
fn from(s: String) -> Value {
Value::Str(s)
}
}
impl From<Duration> for Value {
fn from(d: Duration) -> Value {
Value::Duration(d)
}
}
impl From<Link> for Value {
fn from(l: Link) -> Value {
Value::Link(l)
}
}
impl From<Media> for Value {
fn from(m: Media) -> Value {
Value::Media(m)
}
}
impl From<Props> for Value {
fn from(p: Props) -> Value {
Value::Map(p)
}
}
impl From<Vec<Value>> for Value {
fn from(items: Vec<Value>) -> Value {
Value::List(items)
}
}
fn prop_err(message: String) -> Error {
Error::Property(message)
}
fn type_error(prop: &str, want: ValueKind, found: &Value) -> Error {
prop_err(format!(
"property `{prop}`: expected {want}, found {} {found}",
found.kind()
))
}
fn type_error_hint(prop: &str, want: ValueKind, found: &Value, hint: &str) -> Error {
prop_err(format!(
"property `{prop}`: expected {want}, found {} {found} ({hint})",
found.kind()
))
}
fn at_prop(prop: &str, e: Error) -> Error {
match e {
Error::Property(message) => prop_err(format!("property `{prop}`: {message}")),
other => other,
}
}
fn split_number(s: &str) -> (&str, &str) {
let b = s.as_bytes();
let mut i = 0;
if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
i += 1;
}
let mut radix = 10;
if i + 2 <= b.len() && b[i] == b'0' {
let marker = match b[i + 1] {
b'x' | b'X' => Some(16),
b'b' | b'B' => Some(2),
b'o' | b'O' => Some(8),
_ => None,
};
if let Some(r) = marker
&& b.len() > i + 2
&& (b[i + 2] as char).is_digit(r)
{
radix = r;
i += 2;
}
}
while i < b.len() {
let c = b[i] as char;
if c == '_' || c.is_digit(radix) {
i += 1;
} else {
break;
}
}
s.split_at(i)
}
fn digits_to_u64(orig: &str, num: &str, what: &str) -> Result<(bool, u64)> {
let mut s = num;
let mut negative = false;
if let Some(rest) = s.strip_prefix('-') {
negative = true;
s = rest;
} else if let Some(rest) = s.strip_prefix('+') {
s = rest;
}
let mut radix: u32 = 10;
for (prefix, r) in [
("0x", 16),
("0X", 16),
("0b", 2),
("0B", 2),
("0o", 8),
("0O", 8),
] {
if let Some(rest) = s.strip_prefix(prefix)
&& rest.starts_with(|c: char| c.is_digit(r))
{
radix = r;
s = rest;
break;
}
}
if s.is_empty() {
return Err(prop_err(format!(
"`{orig}` is not a valid {what}: no digits"
)));
}
if s.starts_with('_') || s.ends_with('_') {
return Err(prop_err(format!(
"`{orig}` is not a valid {what}: `_` may only separate digits"
)));
}
let mut value: u64 = 0;
let mut digits = 0usize;
for c in s.chars() {
if c == '_' {
continue;
}
let d = c.to_digit(radix).ok_or_else(|| {
prop_err(format!(
"`{orig}` is not a valid {what}: `{c}` is not a base-{radix} digit"
))
})?;
value = value
.checked_mul(radix as u64)
.and_then(|v| v.checked_add(d as u64))
.ok_or_else(|| {
prop_err(format!(
"`{orig}` is not a valid {what}: value overflows 64 bits"
))
})?;
digits += 1;
}
if digits == 0 {
return Err(prop_err(format!(
"`{orig}` is not a valid {what}: no digits"
)));
}
Ok((negative, value))
}
fn parse_unsigned(text: &str, what: &str) -> Result<u64> {
let s = text.trim();
let (num, rest) = split_number(s);
if !rest.is_empty() {
return Err(prop_err(format!(
"`{text}` is not a valid {what}: unexpected `{rest}`"
)));
}
let (negative, value) = digits_to_u64(text, num, what)?;
if negative && value != 0 {
return Err(prop_err(format!(
"`{text}` is not a valid {what}: it is negative"
)));
}
Ok(value)
}
pub fn parse_uint(text: &str) -> Result<u64> {
parse_unsigned(text, "unsigned integer")
}
pub fn parse_addr(text: &str) -> Result<u64> {
parse_unsigned(text, "address")
}
pub fn parse_int(text: &str) -> Result<i64> {
let what = "signed integer";
let s = text.trim();
let (num, rest) = split_number(s);
if !rest.is_empty() {
return Err(prop_err(format!(
"`{text}` is not a valid {what}: unexpected `{rest}`"
)));
}
let (negative, magnitude) = digits_to_u64(text, num, what)?;
let wide = if negative {
-(magnitude as i128)
} else {
magnitude as i128
};
i64::try_from(wide).map_err(|_| {
prop_err(format!(
"`{text}` is not a valid {what}: value overflows 64 bits"
))
})
}
pub fn parse_bool(text: &str) -> Result<bool> {
let s = text.trim();
if s.eq_ignore_ascii_case("true")
|| s.eq_ignore_ascii_case("yes")
|| s.eq_ignore_ascii_case("on")
{
return Ok(true);
}
if s.eq_ignore_ascii_case("false")
|| s.eq_ignore_ascii_case("no")
|| s.eq_ignore_ascii_case("off")
{
return Ok(false);
}
Err(prop_err(format!(
"`{text}` is not a valid boolean (expected true/false, yes/no or on/off)"
)))
}
pub fn parse_size(text: &str) -> Result<u64> {
let what = "size";
let s = text.trim();
let (num, suffix) = split_number(s);
let (negative, value) = digits_to_u64(text, num, what)?;
if negative && value != 0 {
return Err(prop_err(format!(
"`{text}` is not a valid {what}: a byte count cannot be negative"
)));
}
let mult = size_multiplier(text, suffix)?;
value.checked_mul(mult).ok_or_else(|| {
prop_err(format!(
"`{text}` is not a valid {what}: it overflows a 64-bit byte count"
))
})
}
fn size_multiplier(orig: &str, suffix: &str) -> Result<u64> {
if suffix.is_empty() {
return Ok(1);
}
let bad = || {
prop_err(format!(
"`{orig}` is not a valid size: unknown suffix `{suffix}` \
(expected K, M, G, T, P or E, optionally followed by i and/or B — all binary)"
))
};
let mut chars = suffix.chars();
let head = chars.next().ok_or_else(bad)?.to_ascii_lowercase();
let tail: String = chars.flat_map(|c| c.to_lowercase()).collect();
if head == 'b' {
return if tail.is_empty() { Ok(1) } else { Err(bad()) };
}
let exponent = match head {
'k' => 1u32,
'm' => 2,
'g' => 3,
't' => 4,
'p' => 5,
'e' => 6,
_ => return Err(bad()),
};
match tail.as_str() {
"" | "i" | "b" | "ib" => {}
_ => return Err(bad()),
}
Ok(1u64 << (10 * exponent))
}
pub fn parse_duration(text: &str) -> Result<Duration> {
let what = "duration";
let s = text.trim();
if s.is_empty() {
return Err(prop_err(format!("`{text}` is not a valid {what}: empty")));
}
if s.starts_with('-') {
return Err(prop_err(format!(
"`{text}` is not a valid {what}: it is negative"
)));
}
let mut total: u64 = 0;
let mut rest = s;
while !rest.is_empty() {
let (num, after) = split_number(rest);
if num.is_empty() {
return Err(prop_err(format!(
"`{text}` is not a valid {what}: expected a number at `{rest}`"
)));
}
let (_, value) = digits_to_u64(text, num, what)?;
let unit_len = after
.find(|c: char| !c.is_alphabetic())
.unwrap_or(after.len());
let (unit, tail) = after.split_at(unit_len);
if unit.is_empty() {
return Err(prop_err(format!(
"`{text}` is not a valid {what}: `{num}` has no unit \
(expected ps, ns, us, ms, s, m or h)"
)));
}
let scale = duration_scale(unit).ok_or_else(|| {
prop_err(format!(
"`{text}` is not a valid {what}: unknown unit `{unit}` \
(expected ps, ns, us, ms, s, m or h)"
))
})?;
let overflow = || {
prop_err(format!(
"`{text}` is not a valid {what}: it overflows a 64-bit picosecond count"
))
};
total = value
.checked_mul(scale)
.and_then(|part| total.checked_add(part))
.ok_or_else(overflow)?;
rest = tail;
}
Ok(Duration::from_picos(total))
}
fn duration_scale(unit: &str) -> Option<u64> {
let lower: String = unit.chars().flat_map(|c| c.to_lowercase()).collect();
Some(match lower.as_str() {
"ps" => 1,
"ns" => 1_000,
"us" | "\u{b5}s" | "\u{3bc}s" => 1_000_000,
"ms" => 1_000_000_000,
"s" | "sec" => 1_000_000_000_000,
"m" | "min" => 60_000_000_000_000,
"h" | "hr" => 3_600_000_000_000_000,
_ => return None,
})
}
pub trait FromValue: Sized {
const EXPECTED: &'static str;
fn from_value(prop: &str, value: &Value) -> Result<Self>;
}
impl FromValue for bool {
const EXPECTED: &'static str = "boolean";
fn from_value(prop: &str, value: &Value) -> Result<Self> {
value.to_bool(prop)
}
}
impl FromValue for u64 {
const EXPECTED: &'static str = "unsigned integer";
fn from_value(prop: &str, value: &Value) -> Result<Self> {
value.to_uint(prop)
}
}
impl FromValue for i64 {
const EXPECTED: &'static str = "signed integer";
fn from_value(prop: &str, value: &Value) -> Result<Self> {
value.to_int(prop)
}
}
macro_rules! impl_from_value_narrow {
($($t:ty => $via:ty, $name:literal;)*) => {$(
impl FromValue for $t {
const EXPECTED: &'static str = $name;
fn from_value(prop: &str, value: &Value) -> Result<Self> {
let wide = <$via as FromValue>::from_value(prop, value)?;
<$t>::try_from(wide).map_err(|_| {
prop_err(format!(
"property `{prop}`: {wide} does not fit in a {}",
$name
))
})
}
}
)*};
}
impl_from_value_narrow! {
u8 => u64, "8-bit unsigned integer";
u16 => u64, "16-bit unsigned integer";
u32 => u64, "32-bit unsigned integer";
i8 => i64, "8-bit signed integer";
i16 => i64, "16-bit signed integer";
i32 => i64, "32-bit signed integer";
}
impl FromValue for String {
const EXPECTED: &'static str = "string";
fn from_value(prop: &str, value: &Value) -> Result<Self> {
value.to_str(prop).map(ToOwned::to_owned)
}
}
impl FromValue for Duration {
const EXPECTED: &'static str = "duration";
fn from_value(prop: &str, value: &Value) -> Result<Self> {
value.to_duration(prop)
}
}
impl FromValue for Link {
const EXPECTED: &'static str = "link";
fn from_value(prop: &str, value: &Value) -> Result<Self> {
value.to_link(prop).cloned()
}
}
impl FromValue for Media {
const EXPECTED: &'static str = "media";
fn from_value(prop: &str, value: &Value) -> Result<Self> {
value.to_media(prop).cloned()
}
}
impl FromValue for Props {
const EXPECTED: &'static str = "map";
fn from_value(prop: &str, value: &Value) -> Result<Self> {
value.to_map(prop).cloned()
}
}
impl FromValue for Vec<Value> {
const EXPECTED: &'static str = "list";
fn from_value(prop: &str, value: &Value) -> Result<Self> {
value.to_list(prop).map(<[Value]>::to_vec)
}
}
impl FromValue for Value {
const EXPECTED: &'static str = "value";
fn from_value(_prop: &str, value: &Value) -> Result<Self> {
Ok(value.clone())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Props {
entries: Vec<(String, Value)>,
}
impl Props {
pub fn new() -> Props {
Props {
entries: Vec::new(),
}
}
pub fn insert(&mut self, name: impl Into<String>, value: impl Into<Value>) {
let name = name.into();
let value = value.into();
for entry in &mut self.entries {
if entry.0 == name {
entry.1 = value;
return;
}
}
self.entries.push((name, value));
}
#[must_use]
pub fn with(mut self, name: impl Into<String>, value: impl Into<Value>) -> Props {
self.insert(name, value);
self
}
pub fn remove(&mut self, name: &str) -> Option<Value> {
let index = self.entries.iter().position(|(n, _)| n == name)?;
Some(self.entries.remove(index).1)
}
pub fn get(&self, name: &str) -> Option<&Value> {
self.entries.iter().find(|(n, _)| n == name).map(|(_, v)| v)
}
pub fn require(&self, name: &str) -> Result<&Value> {
self.get(name)
.ok_or_else(|| prop_err(format!("missing required property `{name}`")))
}
pub fn contains(&self, name: &str) -> bool {
self.get(name).is_some()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
self.entries.iter().map(|(n, v)| (n.as_str(), v))
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.entries.iter().map(|(n, _)| n.as_str())
}
pub fn check_known(&self, allowed: &[&str]) -> Result<()> {
let unknown: Vec<&str> = self.names().filter(|n| !allowed.contains(n)).collect();
unknown_error(&unknown, allowed)
}
pub fn reader(&self) -> Reader<'_> {
Reader::new(self)
}
}
impl FromIterator<(String, Value)> for Props {
fn from_iter<I: IntoIterator<Item = (String, Value)>>(iter: I) -> Props {
let mut props = Props::new();
for (name, value) in iter {
props.insert(name, value);
}
props
}
}
impl Extend<(String, Value)> for Props {
fn extend<I: IntoIterator<Item = (String, Value)>>(&mut self, iter: I) {
for (name, value) in iter {
self.insert(name, value);
}
}
}
impl IntoIterator for Props {
type Item = (String, Value);
type IntoIter = alloc::vec::IntoIter<(String, Value)>;
fn into_iter(self) -> Self::IntoIter {
self.entries.into_iter()
}
}
impl<'a> IntoIterator for &'a Props {
type Item = (&'a str, &'a Value);
type IntoIter = core::iter::Map<
core::slice::Iter<'a, (String, Value)>,
fn(&'a (String, Value)) -> (&'a str, &'a Value),
>;
fn into_iter(self) -> Self::IntoIter {
fn split(entry: &(String, Value)) -> (&str, &Value) {
(entry.0.as_str(), &entry.1)
}
self.entries.iter().map(split as fn(_) -> _)
}
}
#[derive(Debug)]
pub struct Reader<'a> {
props: &'a Props,
seen: Vec<bool>,
asked: Vec<String>,
}
impl<'a> Reader<'a> {
pub fn new(props: &'a Props) -> Reader<'a> {
Reader {
props,
seen: alloc::vec![false; props.len()],
asked: Vec::new(),
}
}
pub fn props(&self) -> &'a Props {
self.props
}
fn lookup(&mut self, name: &str) -> Option<&'a Value> {
if !self.asked.iter().any(|n| n == name) {
self.asked.push(name.to_owned());
}
let index = self.props.entries.iter().position(|(n, _)| n == name)?;
if let Some(seen) = self.seen.get_mut(index) {
*seen = true;
}
self.props.entries.get(index).map(|(_, v)| v)
}
pub fn touch(&mut self, name: &str) -> Option<&'a Value> {
self.lookup(name)
}
pub fn require<T: FromValue>(&mut self, name: &str) -> Result<T> {
match self.lookup(name) {
Some(value) => T::from_value(name, value),
None => Err(prop_err(format!(
"missing required property `{name}` (expected {})",
T::EXPECTED
))),
}
}
pub fn optional<T: FromValue>(&mut self, name: &str) -> Result<Option<T>> {
match self.lookup(name) {
Some(value) => T::from_value(name, value).map(Some),
None => Ok(None),
}
}
pub fn or<T: FromValue>(&mut self, name: &str, default: T) -> Result<T> {
Ok(self.optional(name)?.unwrap_or(default))
}
pub fn require_range<T>(&mut self, name: &str, range: RangeInclusive<T>) -> Result<T>
where
T: FromValue + PartialOrd + fmt::Display,
{
let value: T = self.require(name)?;
check_range(name, value, range)
}
pub fn or_range<T>(&mut self, name: &str, default: T, range: RangeInclusive<T>) -> Result<T>
where
T: FromValue + PartialOrd + fmt::Display,
{
let value = self.or(name, default)?;
check_range(name, value, range)
}
pub fn require_str(&mut self, name: &str) -> Result<&'a str> {
match self.lookup(name) {
Some(value) => value.to_str(name),
None => Err(prop_err(format!(
"missing required property `{name}` (expected string)"
))),
}
}
pub fn optional_str(&mut self, name: &str) -> Result<Option<&'a str>> {
match self.lookup(name) {
Some(value) => value.to_str(name).map(Some),
None => Ok(None),
}
}
pub fn or_str(&mut self, name: &str, default: &'a str) -> Result<&'a str> {
Ok(self.optional_str(name)?.unwrap_or(default))
}
pub fn require_size(&mut self, name: &str) -> Result<u64> {
match self.lookup(name) {
Some(value) => value.to_size(name),
None => Err(prop_err(format!(
"missing required property `{name}` (expected size)"
))),
}
}
pub fn or_size(&mut self, name: &str, default: u64) -> Result<u64> {
match self.lookup(name) {
Some(value) => value.to_size(name),
None => Ok(default),
}
}
pub fn require_addr(&mut self, name: &str) -> Result<u64> {
match self.lookup(name) {
Some(value) => value.to_addr(name),
None => Err(prop_err(format!(
"missing required property `{name}` (expected address)"
))),
}
}
pub fn or_addr(&mut self, name: &str, default: u64) -> Result<u64> {
match self.lookup(name) {
Some(value) => value.to_addr(name),
None => Ok(default),
}
}
pub fn require_list(&mut self, name: &str) -> Result<&'a [Value]> {
match self.lookup(name) {
Some(value) => value.to_list(name),
None => Err(prop_err(format!(
"missing required property `{name}` (expected list)"
))),
}
}
pub fn optional_list(&mut self, name: &str) -> Result<Option<&'a [Value]>> {
match self.lookup(name) {
Some(value) => value.to_list(name).map(Some),
None => Ok(None),
}
}
pub fn require_map(&mut self, name: &str) -> Result<&'a Props> {
match self.lookup(name) {
Some(value) => value.to_map(name),
None => Err(prop_err(format!(
"missing required property `{name}` (expected map)"
))),
}
}
pub fn optional_map(&mut self, name: &str) -> Result<Option<&'a Props>> {
match self.lookup(name) {
Some(value) => value.to_map(name).map(Some),
None => Ok(None),
}
}
pub fn require_media(&mut self, name: &str) -> Result<&'a Media> {
match self.lookup(name) {
Some(value) => value.to_media(name),
None => Err(prop_err(format!(
"missing required property `{name}` (expected media: name a slot and bind it, \
as in `{name} = \"cart\"` with `--{name} <file>`)"
))),
}
}
pub fn optional_media(&mut self, name: &str) -> Result<Option<&'a Media>> {
match self.lookup(name) {
Some(value) => value.to_media(name).map(Some),
None => Ok(None),
}
}
pub fn require_link(&mut self, name: &str) -> Result<&'a Link> {
match self.lookup(name) {
Some(value) => value.to_link(name),
None => Err(prop_err(format!(
"missing required property `{name}` (expected link)"
))),
}
}
pub fn optional_link(&mut self, name: &str) -> Result<Option<&'a Link>> {
match self.lookup(name) {
Some(value) => value.to_link(name).map(Some),
None => Ok(None),
}
}
pub fn require_enum(&mut self, name: &str, allowed: &[&str]) -> Result<&'a str> {
let value = self.require_str(name)?;
check_enum(name, value, allowed)
}
pub fn or_enum(&mut self, name: &str, default: &'a str, allowed: &[&str]) -> Result<&'a str> {
let value = self.or_str(name, default)?;
check_enum(name, value, allowed)
}
pub fn unused(&self) -> Vec<&'a str> {
self.props
.entries
.iter()
.enumerate()
.filter(|(i, _)| !self.seen.get(*i).copied().unwrap_or(false))
.map(|(_, (name, _))| name.as_str())
.collect()
}
pub fn finish(self) -> Result<()> {
let unused = self.unused();
let known: Vec<&str> = self.asked.iter().map(String::as_str).collect();
unknown_error(&unused, &known)
}
}
fn unknown_error(unknown: &[&str], known: &[&str]) -> Result<()> {
if unknown.is_empty() {
return Ok(());
}
let mut message = String::new();
message.push_str(if unknown.len() == 1 {
"unknown property "
} else {
"unknown properties "
});
for (i, name) in unknown.iter().enumerate() {
if i != 0 {
message.push_str(", ");
}
message.push_str(&format!("`{name}`"));
if let Some(suggestion) = suggest(name, known) {
message.push_str(&format!(" (did you mean `{suggestion}`?)"));
}
}
if known.is_empty() {
message.push_str("; this object takes no properties");
} else {
message.push_str("; known properties: ");
for (i, name) in known.iter().enumerate() {
if i != 0 {
message.push_str(", ");
}
message.push_str(&format!("`{name}`"));
}
}
Err(prop_err(message))
}
pub fn check_range<T>(prop: &str, value: T, range: RangeInclusive<T>) -> Result<T>
where
T: PartialOrd + fmt::Display,
{
if range.contains(&value) {
Ok(value)
} else {
Err(prop_err(format!(
"property `{prop}`: {value} is out of range {}..={}",
range.start(),
range.end()
)))
}
}
pub fn check_enum<'a>(prop: &str, value: &'a str, allowed: &[&str]) -> Result<&'a str> {
if allowed.contains(&value) {
return Ok(value);
}
let mut message = format!("property `{prop}`: expected one of ");
for (i, name) in allowed.iter().enumerate() {
if i != 0 {
message.push_str(", ");
}
message.push_str(&format!("`{name}`"));
}
message.push_str(&format!("; found \"{value}\""));
if let Some(suggestion) = suggest(value, allowed) {
message.push_str(&format!(" (did you mean `{suggestion}`?)"));
}
Err(prop_err(message))
}
pub fn suggest<'a>(name: &str, candidates: &[&'a str]) -> Option<&'a str> {
let limit = (name.chars().count() / 3).max(1);
let mut best: Option<(usize, &str)> = None;
for candidate in candidates {
let distance = edit_distance(name, candidate);
if distance <= limit && best.is_none_or(|(d, _)| distance < d) {
best = Some((distance, candidate));
}
}
best.map(|(_, candidate)| candidate)
}
fn edit_distance(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().flat_map(char::to_lowercase).collect();
let b: Vec<char> = b.chars().flat_map(char::to_lowercase).collect();
let (n, m) = (a.len(), b.len());
if n == 0 {
return m;
}
if m == 0 {
return n;
}
let width = m + 1;
let mut d = alloc::vec![0usize; (n + 1) * width];
for i in 0..=n {
d[i * width] = i;
}
for (j, slot) in d.iter_mut().take(width).enumerate() {
*slot = j;
}
for i in 1..=n {
for j in 1..=m {
let cost = usize::from(a[i - 1] != b[j - 1]);
let mut best = (d[(i - 1) * width + j] + 1)
.min(d[i * width + j - 1] + 1)
.min(d[(i - 1) * width + j - 1] + cost);
if i > 1 && j > 1 && a[i - 1] == b[j - 2] && a[i - 2] == b[j - 1] {
best = best.min(d[(i - 2) * width + j - 2] + 1);
}
d[i * width + j] = best;
}
}
d[n * width + m]
}
pub fn parse_as(prop: &str, kind: ValueKind, text: &str) -> Result<Value> {
let value = match kind {
ValueKind::Int => parse_int(text).map(Value::Int),
ValueKind::Uint => parse_uint(text).map(Value::Uint),
ValueKind::Bool => parse_bool(text).map(Value::Bool),
ValueKind::Str => Ok(Value::Str(text.to_owned())),
ValueKind::Size => parse_size(text).map(Value::Size),
ValueKind::Addr => parse_addr(text).map(Value::Addr),
ValueKind::Duration => parse_duration(text).map(Value::Duration),
ValueKind::Link => Link::new(text).map(Value::Link),
ValueKind::List | ValueKind::Map => Err(prop_err(format!(
"a {kind} cannot be written as a bare scalar"
))),
ValueKind::Media => Err(prop_err(String::from(
"media is bound by name at realize time, not written in a file",
))),
};
value.map_err(|e| at_prop(prop, e))
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
use alloc::vec;
#[test]
fn every_size_suffix_is_binary() {
assert_eq!(parse_size("0").unwrap(), 0);
assert_eq!(parse_size("4096").unwrap(), 4096);
assert_eq!(parse_size("4K").unwrap(), 4 * 1024);
assert_eq!(parse_size("2K").unwrap(), 2048);
assert_eq!(parse_size("512M").unwrap(), 512 * 1024 * 1024);
assert_eq!(parse_size("2G").unwrap(), 2 * 1024 * 1024 * 1024);
assert_eq!(parse_size("1T").unwrap(), 1u64 << 40);
assert_eq!(parse_size("1P").unwrap(), 1u64 << 50);
assert_eq!(parse_size("1E").unwrap(), 1u64 << 60);
assert_eq!(parse_size("8E").unwrap(), 8u64 << 60);
}
#[test]
fn size_suffix_spellings_agree() {
for spelling in ["1K", "1k", "1KiB", "1kib", "1KB", "1kb", "1Ki", "1ki"] {
assert_eq!(parse_size(spelling).unwrap(), 1024, "{spelling}");
}
assert_eq!(parse_size("7B").unwrap(), 7);
assert_eq!(parse_size("0B").unwrap(), 0);
assert_eq!(parse_size("0x10K").unwrap(), 16 * 1024);
assert_eq!(parse_size("0x2000").unwrap(), 0x2000);
}
#[test]
fn size_overflow_is_rejected_not_wrapped() {
let e = parse_size("16E").unwrap_err().to_string();
assert!(e.contains("16E"), "{e}");
assert!(e.contains("overflow"), "{e}");
assert!(parse_size("32E").is_err());
assert!(parse_size("18446744073709551616").is_err());
assert_eq!(parse_size("15E").unwrap(), 15u64 << 60);
}
#[test]
fn a_bad_size_suffix_says_what_is_allowed() {
let e = parse_size("4Q").unwrap_err().to_string();
assert!(e.contains("`4Q`"), "{e}");
assert!(e.contains("unknown suffix `Q`"), "{e}");
assert!(e.contains("K, M, G, T, P or E"), "{e}");
assert!(parse_size("1Kx").is_err());
assert!(parse_size("1BB").is_err());
assert!(parse_size("-4K").is_err());
assert!(parse_size("K").is_err());
}
#[test]
fn number_radix_forms() {
assert_eq!(parse_uint("0x1234").unwrap(), 0x1234);
assert_eq!(parse_uint("0X1234").unwrap(), 0x1234);
assert_eq!(parse_uint("0xdeadBEEF").unwrap(), 0xdead_beef);
assert_eq!(parse_uint("0b1010_0101").unwrap(), 0xa5);
assert_eq!(parse_uint("0o755").unwrap(), 0o755);
assert_eq!(parse_uint("0O755").unwrap(), 0o755);
assert_eq!(parse_uint("1_000_000").unwrap(), 1_000_000);
assert_eq!(parse_uint(" 42 ").unwrap(), 42);
assert_eq!(parse_uint("0755").unwrap(), 755);
assert_eq!(parse_uint("0").unwrap(), 0);
}
#[test]
fn number_errors_are_specific() {
let e = parse_uint("0xzz").unwrap_err().to_string();
assert!(e.contains("0xzz"), "{e}");
let e = parse_uint("_1").unwrap_err().to_string();
assert!(e.contains("`_` may only separate digits"), "{e}");
assert!(parse_uint("1_").is_err());
assert!(parse_uint("12x").is_err());
assert!(parse_uint("").is_err());
assert!(parse_uint("-1").is_err());
assert_eq!(parse_uint("1__0").unwrap(), 10);
let e = parse_uint("-1").unwrap_err().to_string();
assert!(e.contains("negative"), "{e}");
}
#[test]
fn integer_overflow_is_rejected_at_both_ends() {
assert_eq!(parse_uint("18446744073709551615").unwrap(), u64::MAX);
assert!(parse_uint("18446744073709551616").is_err());
assert_eq!(parse_int("-9223372036854775808").unwrap(), i64::MIN);
assert_eq!(parse_int("9223372036854775807").unwrap(), i64::MAX);
assert!(parse_int("9223372036854775808").is_err());
assert!(parse_int("-9223372036854775809").is_err());
assert_eq!(parse_int("+7").unwrap(), 7);
assert_eq!(parse_int("-0x10").unwrap(), -16);
}
#[test]
fn addresses_use_the_number_syntax() {
assert_eq!(parse_addr("0x1234").unwrap(), 0x1234);
assert_eq!(parse_addr("0xffff_ffff").unwrap(), 0xffff_ffff);
let e = parse_addr("nowhere").unwrap_err().to_string();
assert!(e.contains("address"), "{e}");
}
#[test]
fn booleans_accept_the_usual_spellings_but_not_numbers() {
for t in ["true", "TRUE", "yes", "on", " True "] {
assert!(parse_bool(t).unwrap(), "{t}");
}
for f in ["false", "No", "OFF"] {
assert!(!parse_bool(f).unwrap(), "{f}");
}
assert!(parse_bool("1").is_err());
let e = parse_bool("maybe").unwrap_err().to_string();
assert!(e.contains("true/false"), "{e}");
}
#[test]
fn durations_parse_every_unit() {
assert_eq!(parse_duration("1ps").unwrap().as_picos(), 1);
assert_eq!(parse_duration("1ns").unwrap().as_picos(), 1_000);
assert_eq!(parse_duration("1us").unwrap().as_nanos(), 1_000);
assert_eq!(parse_duration("1\u{b5}s").unwrap().as_nanos(), 1_000);
assert_eq!(parse_duration("10ms").unwrap().as_nanos(), 10_000_000);
assert_eq!(parse_duration("2s").unwrap().as_millis(), 2_000);
assert_eq!(parse_duration("3m").unwrap().as_secs(), 180);
assert_eq!(parse_duration("1h").unwrap().as_secs(), 3600);
assert_eq!(parse_duration("1h30m").unwrap().as_secs(), 5400);
assert_eq!(parse_duration("1s500ms").unwrap().as_millis(), 1500);
}
#[test]
fn a_duration_without_a_unit_is_an_error() {
let e = parse_duration("10").unwrap_err().to_string();
assert!(e.contains("no unit"), "{e}");
assert!(e.contains("ms"), "{e}");
let e = parse_duration("10fortnights").unwrap_err().to_string();
assert!(e.contains("unknown unit"), "{e}");
assert!(parse_duration("-1s").is_err());
assert!(parse_duration("").is_err());
assert!(parse_duration("5000h").unwrap().as_secs() > 0);
let e = parse_duration("9000h").unwrap_err().to_string();
assert!(e.contains("overflow"), "{e}");
}
#[test]
fn wrong_type_names_the_property_the_expectation_and_the_find() {
let v = Value::Str("big".into());
let e = v.to_size("size").unwrap_err().to_string();
assert!(e.contains("`size`"), "{e}");
assert!(e.contains("expected size"), "{e}");
assert!(e.contains("found string"), "{e}");
assert!(e.contains("\"big\""), "{e}");
let e = Value::List(vec![])
.to_uint("count")
.unwrap_err()
.to_string();
assert!(
e.contains("`count`") && e.contains("expected unsigned integer"),
"{e}"
);
assert!(e.contains("found list"), "{e}");
let e = Value::Bool(true).to_link("space").unwrap_err().to_string();
assert!(
e.contains("expected link") && e.contains("found boolean"),
"{e}"
);
}
#[test]
fn common_mistakes_get_a_hint() {
let e = Value::Str("true".into())
.to_bool("readonly")
.unwrap_err()
.to_string();
assert!(e.contains("without quotes"), "{e}");
let e = Value::Uint(10)
.to_duration("timeout")
.unwrap_err()
.to_string();
assert!(e.contains("need a unit"), "{e}");
assert!(e.contains("10ms"), "{e}");
let e = Value::Int(-1).to_size("size").unwrap_err().to_string();
assert!(e.contains("negative"), "{e}");
}
#[test]
fn numeric_kinds_coerce_but_strings_never_do() {
assert_eq!(Value::Uint(5).to_size("s").unwrap(), 5);
assert_eq!(Value::Size(5).to_uint("s").unwrap(), 5);
assert_eq!(Value::Addr(5).to_size("s").unwrap(), 5);
assert_eq!(Value::Int(5).to_uint("s").unwrap(), 5);
assert_eq!(Value::Uint(5).to_int("s").unwrap(), 5);
assert!(Value::Str("5".into()).to_uint("s").is_err());
let e = Value::Uint(u64::MAX).to_int("s").unwrap_err().to_string();
assert!(e.contains("does not fit"), "{e}");
}
#[test]
fn narrow_integers_range_check_instead_of_truncating() {
let props = Props::new().with("width", 70000u32);
let mut r = props.reader();
let e = r.require::<u16>("width").unwrap_err().to_string();
assert!(e.contains("70000"), "{e}");
assert!(e.contains("16-bit unsigned integer"), "{e}");
}
#[test]
fn iteration_follows_insertion_order() {
let mut props = Props::new();
for name in ["zeta", "alpha", "middle", "beta"] {
props.insert(name, 1u64);
}
assert_eq!(
props.names().collect::<Vec<_>>(),
["zeta", "alpha", "middle", "beta"]
);
props.insert("alpha", 2u64);
assert_eq!(
props.names().collect::<Vec<_>>(),
["zeta", "alpha", "middle", "beta"]
);
assert_eq!(props.get("alpha"), Some(&Value::Uint(2)));
assert_eq!(props.len(), 4);
props.remove("middle");
assert_eq!(props.names().collect::<Vec<_>>(), ["zeta", "alpha", "beta"]);
}
#[test]
fn iteration_order_is_stable_across_identical_builds() {
let build = || {
Props::new()
.with("size", Value::Size(2048))
.with("base", Value::Addr(0x8000))
.with("name", "wram")
};
let a: Vec<_> = build().names().map(ToOwned::to_owned).collect();
let b: Vec<_> = build().names().map(ToOwned::to_owned).collect();
assert_eq!(a, b);
assert_eq!(a, ["size", "base", "name"]);
}
#[test]
fn missing_required_property_says_what_it_wanted() {
let props = Props::new();
let mut r = props.reader();
let e = r.require::<u64>("clock").unwrap_err().to_string();
assert!(e.contains("missing required property `clock`"), "{e}");
assert!(e.contains("unsigned integer"), "{e}");
assert!(Props::new().require("clock").is_err());
}
#[test]
fn defaults_and_optionals() {
let props = Props::new().with("size", Value::Size(4096));
let mut r = props.reader();
assert_eq!(r.require_size("size").unwrap(), 4096);
assert_eq!(r.or_size("stride", 16).unwrap(), 16);
assert!(!r.or("readonly", false).unwrap());
assert_eq!(r.or_str("name", "unnamed").unwrap(), "unnamed");
assert_eq!(r.optional::<u64>("irq").unwrap(), None);
r.finish().unwrap();
}
#[test]
fn a_typo_is_an_error_with_a_suggestion() {
let props = Props::new()
.with("size", Value::Size(2048))
.with("clok", Value::Uint(12));
let mut r = props.reader();
let _ = r.require_size("size").unwrap();
let _ = r.or("clock", 0u64).unwrap();
let e = r.finish().unwrap_err().to_string();
assert!(e.contains("unknown property `clok`"), "{e}");
assert!(e.contains("did you mean `clock`?"), "{e}");
assert!(e.contains("known properties: `size`, `clock`"), "{e}");
}
#[test]
fn several_unknowns_are_reported_together_in_order() {
let props = Props::new()
.with("zzz", 1u64)
.with("size", Value::Size(1))
.with("aaa", 1u64);
let mut r = props.reader();
let _ = r.require_size("size").unwrap();
let e = r.finish().unwrap_err().to_string();
assert!(e.starts_with("unknown properties `zzz`, `aaa`"), "{e}");
}
#[test]
fn media_is_cheap_to_clone_and_says_nothing_about_its_contents() {
let image: &[u8] = &[0u8; 4096];
let media = Media::new("cart", image);
assert_eq!(media.name(), "cart");
assert_eq!(media.len(), 4096);
assert!(!media.is_empty());
let shown = format!("{media:?}");
assert!(shown.contains("cart") && shown.contains("4096"), "{shown}");
assert!(shown.len() < 60, "debug output is a summary: {shown}");
let shown = Value::Media(media.clone()).to_string();
assert!(shown.contains("cart") && shown.contains("4096"), "{shown}");
}
#[test]
fn an_unbound_media_slot_is_told_apart_from_a_type_error() {
let named = Props::new().with("rom", "cart");
let e = named.reader().require_media("rom").unwrap_err().to_string();
assert!(e.contains("cart"), "{e}");
assert!(e.contains("nothing is bound"), "{e}");
let wrong = Props::new().with("rom", 4096u64);
let e = wrong.reader().require_media("rom").unwrap_err().to_string();
assert!(e.contains("expected media"), "{e}");
let empty = Props::new();
let e = empty.reader().require_media("rom").unwrap_err().to_string();
assert!(e.contains("--rom"), "{e}");
assert!(empty.reader().optional_media("rom").unwrap().is_none());
}
#[test]
fn media_cannot_be_written_as_text() {
let e = parse_as("rom", ValueKind::Media, "smb.nes")
.unwrap_err()
.to_string();
assert!(e.contains("bound by name"), "{e}");
assert_eq!(ValueKind::Media.as_str(), "media");
}
#[test]
fn check_known_is_the_validate_stage_form() {
let props = Props::new().with("size", Value::Size(1)).with("siez", 1u64);
let e = props
.check_known(&["size", "base"])
.unwrap_err()
.to_string();
assert!(
e.contains("`siez`") && e.contains("did you mean `size`?"),
"{e}"
);
assert!(props.check_known(&["size", "siez"]).is_ok());
let e = Props::new()
.with("anything", 1u64)
.check_known(&[])
.unwrap_err()
.to_string();
assert!(e.contains("takes no properties"), "{e}");
}
#[test]
fn touch_suppresses_the_unknown_report() {
let props = Props::new().with("clock", "master / 12");
let mut r = props.reader();
assert!(r.touch("clock").is_some());
r.finish().unwrap();
}
#[test]
fn suggestions_do_not_fire_on_unrelated_names() {
assert_eq!(suggest("clok", &["clock", "size"]), Some("clock"));
assert_eq!(suggest("engien", &["engine"]), Some("engine"));
assert_eq!(suggest("siez", &["size", "base"]), Some("size"));
assert_eq!(edit_distance("siez", "size"), 1);
assert_eq!(suggest("frobnicate", &["clock", "size"]), None);
assert_eq!(suggest("irq", &["iru"]), Some("iru"));
assert_eq!(suggest("irq", &["dma"]), None);
}
#[test]
fn range_checking_reports_the_bound_it_broke() {
let props = Props::new().with("width", 65u64);
let mut r = props.reader();
let e = r
.require_range::<u64>("width", 1..=64)
.unwrap_err()
.to_string();
assert!(e.contains("`width`"), "{e}");
assert!(e.contains("65 is out of range 1..=64"), "{e}");
let props = Props::new().with("width", 16u64);
let mut r = props.reader();
assert_eq!(r.require_range::<u64>("width", 1..=64).unwrap(), 16);
let props = Props::new();
let mut r = props.reader();
assert_eq!(r.or_range::<u64>("width", 8, 1..=64).unwrap(), 8);
assert!(r.or_range::<u64>("other", 99, 1..=64).is_err());
}
#[test]
fn enum_checking_lists_the_alternatives() {
let props = Props::new().with("engine", "intrep");
let mut r = props.reader();
let e = r
.require_enum("engine", &["interp", "jit", "auto"])
.unwrap_err()
.to_string();
assert!(e.contains("`engine`"), "{e}");
assert!(e.contains("expected one of `interp`, `jit`, `auto`"), "{e}");
assert!(e.contains("found \"intrep\""), "{e}");
assert!(e.contains("did you mean `interp`?"), "{e}");
let props = Props::new().with("engine", "jit");
let mut r = props.reader();
assert_eq!(r.require_enum("engine", &["interp", "jit"]).unwrap(), "jit");
let props = Props::new();
let mut r = props.reader();
assert_eq!(
r.or_enum("engine", "interp", &["interp", "jit"]).unwrap(),
"interp"
);
}
#[test]
fn links_validate_their_path_syntax() {
let link = Link::new("ppu.regs").unwrap();
assert_eq!(link.as_str(), "ppu.regs");
assert_eq!(link.root(), "ppu");
assert_eq!(link.segments().collect::<Vec<_>>(), ["ppu", "regs"]);
assert!(Link::new("cpu0").is_ok());
assert!(Link::new("a-b.c_d").is_ok());
assert!(Link::new("").is_err());
assert!(Link::new("a..b").is_err());
let e = Link::new("a.b!").unwrap_err().to_string();
assert!(e.contains("unexpected `!`"), "{e}");
}
#[test]
fn lists_and_nested_maps_extract() {
let inner = Props::new().with("size", Value::Size(4096));
let props = Props::new()
.with("irqs", vec![Value::Uint(3), Value::Uint(5)])
.with("bar0", inner)
.with("space", Link::new("cpubus").unwrap());
let mut r = props.reader();
assert_eq!(r.require_list("irqs").unwrap().len(), 2);
assert_eq!(
r.require_map("bar0").unwrap().require("size").unwrap(),
&Value::Size(4096)
);
assert_eq!(r.require_link("space").unwrap().as_str(), "cpubus");
r.finish().unwrap();
}
#[test]
fn values_print_the_way_a_machine_file_writes_them() {
assert_eq!(Value::Size(512 * 1024 * 1024).to_string(), "512M");
assert_eq!(Value::Size(1024).to_string(), "1K");
assert_eq!(Value::Size(1536).to_string(), "1536");
assert_eq!(Value::Size(0).to_string(), "0");
assert_eq!(Value::Addr(0x2000).to_string(), "0x2000");
assert_eq!(Value::Str("ntsc".into()).to_string(), "\"ntsc\"");
assert_eq!(Value::Int(-4).to_string(), "-4");
assert_eq!(
Value::List(vec![Value::Uint(3), Value::Uint(5)]).to_string(),
"[3, 5]"
);
assert_eq!(
Value::Map(Props::new().with("size", Value::Size(2048))).to_string(),
"{ size = 2K }"
);
assert_eq!(parse_duration("10ms").unwrap().to_string(), "10ms");
assert_eq!(parse_duration("1h30m").unwrap().to_string(), "90m");
assert_eq!(Duration::ZERO.to_string(), "0s");
}
#[test]
fn scalar_guessing_covers_the_cli_override_case() {
assert_eq!(Value::parse_scalar("4M"), Value::Size(4 * 1024 * 1024));
assert_eq!(Value::parse_scalar("0x8000"), Value::Uint(0x8000));
assert_eq!(Value::parse_scalar("42"), Value::Uint(42));
assert_eq!(Value::parse_scalar("-42"), Value::Int(-42));
assert_eq!(Value::parse_scalar("true"), Value::Bool(true));
assert_eq!(
Value::parse_scalar("10ms"),
Value::Duration(Duration::from_millis(10).unwrap())
);
assert_eq!(Value::parse_scalar("ntsc"), Value::Str("ntsc".into()));
assert_eq!(Value::parse_scalar("4M").to_uint("ram").unwrap(), 4194304);
}
#[test]
fn parse_as_prefixes_the_property_name() {
let e = parse_as("ram", ValueKind::Size, "4Q")
.unwrap_err()
.to_string();
assert!(e.contains("property `ram`:"), "{e}");
assert!(e.contains("unknown suffix `Q`"), "{e}");
assert_eq!(
parse_as("base", ValueKind::Addr, "0x8000").unwrap(),
Value::Addr(0x8000)
);
assert!(parse_as("x", ValueKind::List, "1,2").is_err());
}
#[test]
fn props_are_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Props>();
assert_send_sync::<Value>();
assert_send_sync::<Duration>();
assert_send_sync::<Link>();
}
}