#![cfg(feature = "unstable")]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, doc(cfg(unstable)))]
#![doc(html_logo_url = "https://knurling.ferrous-systems.com/knurling_logo_light_text.svg")]
mod types;
use std::{borrow::Cow, ops::Range, str::FromStr};
pub use crate::types::Type;
#[derive(Clone, Debug, PartialEq)]
pub struct Parameter {
pub index: usize,
pub ty: Type,
pub hint: Option<DisplayHint>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum TimePrecision {
Millis,
Seconds,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DisplayHint {
NoHint {
zero_pad: usize,
},
Hexadecimal {
alternate: bool,
uppercase: bool,
zero_pad: usize,
},
Binary {
alternate: bool,
zero_pad: usize,
},
Ascii,
Debug,
Microseconds,
ISO8601(TimePrecision),
Bitflags {
name: String,
package: String,
disambiguator: String,
},
Unknown(String),
}
fn parse_display_hint(mut s: &str) -> Option<DisplayHint> {
const BITFLAGS_HINT_START: &str = "__internal_bitflags_";
let alternate = if matches!(s.chars().next(), Some('#')) {
s = &s[1..]; true
} else {
false
};
let zero_pad = if let Some(rest) = s.strip_prefix('0') {
let (rest, columns) = parse_integer::<usize>(rest)?;
s = rest;
columns
} else {
0 };
if let Some(stripped) = s.strip_prefix(BITFLAGS_HINT_START) {
let parts = stripped.split('@').collect::<Vec<_>>();
match *parts {
[bitflags_name, package, disambiguator] => {
return Some(DisplayHint::Bitflags {
name: bitflags_name.into(),
package: package.into(),
disambiguator: disambiguator.into(),
});
}
_ => {
return Some(DisplayHint::Unknown(s.into()));
}
}
}
Some(match s {
"" => DisplayHint::NoHint { zero_pad },
"us" => DisplayHint::Microseconds,
"a" => DisplayHint::Ascii,
"b" => DisplayHint::Binary {
alternate,
zero_pad,
},
"x" => DisplayHint::Hexadecimal {
alternate,
uppercase: false,
zero_pad,
},
"X" => DisplayHint::Hexadecimal {
alternate,
uppercase: true,
zero_pad,
},
"iso8601ms" => DisplayHint::ISO8601(TimePrecision::Millis),
"iso8601s" => DisplayHint::ISO8601(TimePrecision::Seconds),
"?" => DisplayHint::Debug,
_ => return None,
})
}
#[derive(Clone, Debug, PartialEq)]
pub enum Fragment<'f> {
Literal(Cow<'f, str>),
Parameter(Parameter),
}
#[derive(Debug, PartialEq)]
struct Param {
index: Option<usize>,
ty: Type,
hint: Option<DisplayHint>,
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub enum Level {
Trace,
Debug,
Info,
Warn,
Error,
}
impl Level {
pub fn as_str(self) -> &'static str {
match self {
Level::Trace => "trace",
Level::Debug => "debug",
Level::Info => "info",
Level::Warn => "warn",
Level::Error => "error",
}
}
}
fn parse_integer<T: FromStr>(s: &str) -> Option<(&str, usize)> {
let start_digits = s
.as_bytes()
.iter()
.copied()
.take_while(|b| b.is_ascii_digit())
.count();
let num = s[..start_digits].parse().ok()?;
Some((&s[start_digits..], num))
}
fn parse_range(mut s: &str) -> Option<(Range<u8>, usize /* consumed */)> {
let start_digits = s
.as_bytes()
.iter()
.take_while(|b| (**b as char).is_ascii_digit())
.count();
let start = s[..start_digits].parse().ok()?;
if &s[start_digits..start_digits + 2] != ".." {
return None;
}
s = &s[start_digits + 2..];
let end_digits = s
.as_bytes()
.iter()
.take_while(|b| (**b as char).is_ascii_digit())
.count();
let end = s[..end_digits].parse().ok()?;
if end <= start || start >= 128 || end > 128 {
return None;
}
Some((start..end, start_digits + end_digits + 2))
}
fn parse_array(mut s: &str) -> Result<usize, Cow<'static, str>> {
let len_pos = s
.find(|c: char| c != ' ')
.ok_or("invalid array specifier (missing length)")?;
s = &s[len_pos..];
let after_len = s
.find(|c: char| !c.is_digit(10))
.ok_or("invalid array specifier (missing `]`)")?;
let len = s[..after_len].parse::<usize>().map_err(|e| e.to_string())?;
s = &s[after_len..];
if s != "]" {
return Err("invalid array specifier (missing `]`)".into());
}
Ok(len)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ParserMode {
Strict,
ForwardsCompatible,
}
fn parse_param(mut input: &str, mode: ParserMode) -> Result<Param, Cow<'static, str>> {
const TYPE_PREFIX: &str = "=";
const HINT_PREFIX: &str = ":";
let mut index = None;
let index_end = input
.find(|c: char| !c.is_digit(10))
.unwrap_or_else(|| input.len());
if index_end != 0 {
index = Some(
input[..index_end]
.parse::<usize>()
.map_err(|e| e.to_string())?,
);
}
let mut ty = Type::default(); input = &input[index_end..];
if input.starts_with(TYPE_PREFIX) {
input = &input[TYPE_PREFIX.len()..];
let type_end = input.find(HINT_PREFIX).unwrap_or_else(|| input.len());
let type_fragment = &input[..type_end];
const FORMAT_ARRAY_START: &str = "[?;";
const U8_ARRAY_START: &str = "[u8;";
ty = match type_fragment.parse() {
Ok(ty) => ty,
_ if input.starts_with(U8_ARRAY_START) => {
let len = parse_array(&type_fragment[U8_ARRAY_START.len()..])?;
Type::U8Array(len)
}
_ if input.starts_with(FORMAT_ARRAY_START) => {
let len = parse_array(&type_fragment[FORMAT_ARRAY_START.len()..])?;
Type::FormatArray(len)
}
_ => match parse_range(type_fragment) {
Some((_, used)) if used != type_fragment.len() => {
return Err("trailing data after bitfield range".into());
}
Some((range, _)) => Type::BitField(range),
None => {
return Err(format!(
"malformed format string (invalid type specifier `{}`)",
input
)
.into());
}
},
};
input = &input[type_end..];
}
let mut hint = None;
if input.starts_with(HINT_PREFIX) {
input = &input[HINT_PREFIX.len()..];
if input.is_empty() {
return Err("malformed format string (missing display hint after ':')".into());
}
hint = Some(match parse_display_hint(input) {
Some(a) => a,
None => match mode {
ParserMode::Strict => {
return Err(format!("unknown display hint: {:?}", input).into());
}
ParserMode::ForwardsCompatible => DisplayHint::Unknown(input.to_owned()),
},
});
} else if !input.is_empty() {
return Err(format!("unexpected content {:?} in format string", input).into());
}
Ok(Param { index, ty, hint })
}
fn push_literal<'f>(
frag: &mut Vec<Fragment<'f>>,
unescaped_literal: &'f str,
) -> Result<(), Cow<'static, str>> {
let mut last_open = false;
let mut last_close = false;
for c in unescaped_literal.chars() {
match c {
'{' => last_open = !last_open,
'}' => last_close = !last_close,
_ => {
if last_open {
return Err("unmatched `{` in format string".into());
}
if last_close {
return Err("unmatched `}` in format string".into());
}
}
}
}
if last_open {
return Err("unmatched `{` in format string".into());
}
if last_close {
return Err("unmatched `}` in format string".into());
}
let literal = unescaped_literal.replace("{{", "{").replace("}}", "}");
frag.push(Fragment::Literal(literal.into()));
Ok(())
}
pub fn get_max_bitfield_range<'a, I>(params: I) -> Option<(u8, u8)>
where
I: Iterator<Item = &'a Parameter> + Clone,
{
let largest_bit_index = params
.clone()
.map(|param| match ¶m.ty {
Type::BitField(range) => range.end,
_ => unreachable!(),
})
.max();
let smallest_bit_index = params
.map(|param| match ¶m.ty {
Type::BitField(range) => range.start,
_ => unreachable!(),
})
.min();
match (smallest_bit_index, largest_bit_index) {
(Some(smallest), Some(largest)) => Some((smallest, largest)),
(None, None) => None,
_ => unreachable!(),
}
}
pub fn parse<'f>(
format_string: &'f str,
mode: ParserMode,
) -> Result<Vec<Fragment<'f>>, Cow<'static, str>> {
let mut fragments = Vec::new();
let mut end_pos = 0;
let mut next_arg_index = 0;
let mut chars = format_string.char_indices();
while let Some((brace_pos, ch)) = chars.next() {
if ch != '{' {
continue;
}
if chars.as_str().starts_with('{') {
chars.next(); continue;
}
if brace_pos > end_pos {
let unescaped_literal = &format_string[end_pos..brace_pos];
push_literal(&mut fragments, unescaped_literal)?;
}
let len = chars
.as_str()
.find('}')
.ok_or("missing `}` in format string")?;
end_pos = brace_pos + 1 + len + 1;
let param_str = &format_string[brace_pos + 1..][..len];
let param = parse_param(param_str, mode)?;
fragments.push(Fragment::Parameter(Parameter {
index: param.index.unwrap_or_else(|| {
let idx = next_arg_index;
next_arg_index += 1;
idx
}),
ty: param.ty,
hint: param.hint,
}));
}
if end_pos != format_string.len() {
push_literal(&mut fragments, &format_string[end_pos..])?;
}
let mut args = Vec::new();
for frag in &fragments {
if let Fragment::Parameter(Parameter { index, ty, .. }) = frag {
if args.len() <= *index {
args.resize(*index + 1, None);
}
match &mut args[*index] {
none @ None => {
*none = Some(ty.clone());
}
Some(other_ty) => match (other_ty, ty) {
(Type::BitField(_), Type::BitField(_)) => {}
(a, b) if a != b => {
return Err(format!(
"conflicting types for argument {}: used as {:?} and {:?}",
index, a, ty
)
.into());
}
_ => {}
},
}
}
}
for (index, arg) in args.iter().enumerate() {
if arg.is_none() {
return Err(format!("argument {} is not used in this format string", index).into());
}
}
Ok(fragments)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_parse_param_cases() {
assert_eq!(
parse_param("", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: None,
})
);
assert_eq!(
parse_param("=u8", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::U8,
hint: None,
})
);
assert_eq!(
parse_param(":a", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: Some(DisplayHint::Ascii),
})
);
assert_eq!(
parse_param("1", ParserMode::Strict),
Ok(Param {
index: Some(1),
ty: Type::Format,
hint: None,
})
);
assert_eq!(
parse_param("=u8:x", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::U8,
hint: Some(DisplayHint::Hexadecimal {
alternate: false,
uppercase: false,
zero_pad: 0
}),
})
);
assert_eq!(
parse_param("0=u8", ParserMode::Strict),
Ok(Param {
index: Some(0),
ty: Type::U8,
hint: None,
})
);
assert_eq!(
parse_param("0:a", ParserMode::Strict),
Ok(Param {
index: Some(0),
ty: Type::Format,
hint: Some(DisplayHint::Ascii),
})
);
assert_eq!(
parse_param("1=u8:b", ParserMode::Strict),
Ok(Param {
index: Some(1),
ty: Type::U8,
hint: Some(DisplayHint::Binary {
alternate: false,
zero_pad: 0,
}),
})
);
}
#[test]
fn all_display_hints() {
assert_eq!(
parse_param(":a", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: Some(DisplayHint::Ascii),
})
);
assert_eq!(
parse_param(":b", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: Some(DisplayHint::Binary {
alternate: false,
zero_pad: 0,
}),
})
);
assert_eq!(
parse_param(":#b", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: Some(DisplayHint::Binary {
alternate: true,
zero_pad: 0,
}),
})
);
assert_eq!(
parse_param(":x", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: Some(DisplayHint::Hexadecimal {
alternate: false,
uppercase: false,
zero_pad: 0
}),
})
);
assert_eq!(
parse_param(":#x", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: Some(DisplayHint::Hexadecimal {
alternate: true,
uppercase: false,
zero_pad: 0
}),
})
);
assert_eq!(
parse_param(":X", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: Some(DisplayHint::Hexadecimal {
alternate: false,
uppercase: true,
zero_pad: 0
}),
})
);
assert_eq!(
parse_param(":#X", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: Some(DisplayHint::Hexadecimal {
alternate: true,
uppercase: true,
zero_pad: 0
}),
})
);
assert_eq!(
parse_param(":iso8601ms", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: Some(DisplayHint::ISO8601(TimePrecision::Millis)),
})
);
assert_eq!(
parse_param(":iso8601s", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: Some(DisplayHint::ISO8601(TimePrecision::Seconds)),
})
);
assert_eq!(
parse_param(":?", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: Some(DisplayHint::Debug),
})
);
assert_eq!(
parse_param(":unknown", ParserMode::ForwardsCompatible),
Ok(Param {
index: None,
ty: Type::Format,
hint: Some(DisplayHint::Unknown("unknown".to_string())),
})
);
}
#[test]
fn all_types() {
assert_eq!(
parse_param("=bool", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Bool,
hint: None,
})
);
assert_eq!(
parse_param("=?", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: None,
})
);
assert_eq!(
parse_param("=i16", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::I16,
hint: None,
})
);
assert_eq!(
parse_param("=i32", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::I32,
hint: None,
})
);
assert_eq!(
parse_param("=i64", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::I64,
hint: None,
})
);
assert_eq!(
parse_param("=i128", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::I128,
hint: None,
})
);
assert_eq!(
parse_param("=i8", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::I8,
hint: None,
})
);
assert_eq!(
parse_param("=str", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Str,
hint: None,
})
);
assert_eq!(
parse_param("=u16", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::U16,
hint: None,
})
);
assert_eq!(
parse_param("=u32", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::U32,
hint: None,
})
);
assert_eq!(
parse_param("=u64", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::U64,
hint: None,
})
);
assert_eq!(
parse_param("=u128", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::U128,
hint: None,
})
);
assert_eq!(
parse_param("=u128:iso8601s", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::U128,
hint: Some(DisplayHint::ISO8601(TimePrecision::Seconds)),
})
);
assert_eq!(
parse_param("=f32", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::F32,
hint: None,
})
);
assert_eq!(
parse_param("=u8", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::U8,
hint: None,
})
);
assert_eq!(
parse_param("=[u8]", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::U8Slice,
hint: None,
})
);
assert_eq!(
parse_param("=usize", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Usize,
hint: None,
})
);
assert_eq!(
parse_param("=isize", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Isize,
hint: None,
})
);
}
#[test]
fn zero_pad() {
assert_eq!(
parse_param(":02", ParserMode::Strict),
Ok(Param {
index: None,
ty: Type::Format,
hint: Some(DisplayHint::NoHint { zero_pad: 2 })
})
)
}
#[test]
fn index() {
assert_eq!(
parse("{=u8}{=u16}", ParserMode::Strict),
Ok(vec![
Fragment::Parameter(Parameter {
index: 0,
ty: Type::U8,
hint: None,
}),
Fragment::Parameter(Parameter {
index: 1,
ty: Type::U16,
hint: None,
}),
])
);
assert_eq!(
parse("{=u8}{0=u8}", ParserMode::Strict),
Ok(vec![
Fragment::Parameter(Parameter {
index: 0,
ty: Type::U8,
hint: None,
}),
Fragment::Parameter(Parameter {
index: 0,
ty: Type::U8,
hint: None,
}),
])
);
assert_eq!(
parse("{=u8}{1=u16}", ParserMode::Strict),
Ok(vec![
Fragment::Parameter(Parameter {
index: 0,
ty: Type::U8,
hint: None,
}),
Fragment::Parameter(Parameter {
index: 1,
ty: Type::U16,
hint: None,
}),
])
);
assert_eq!(
parse("{1=u8}{0=u16}", ParserMode::Strict),
Ok(vec![
Fragment::Parameter(Parameter {
index: 1,
ty: Type::U8,
hint: None,
}),
Fragment::Parameter(Parameter {
index: 0,
ty: Type::U16,
hint: None,
}),
])
);
assert!(parse("{0=u8}{0=u16}", ParserMode::Strict).is_err());
assert!(parse("Hello {1=u16} {0=u8} {=bool}", ParserMode::Strict).is_err());
assert!(parse("{1=u8}", ParserMode::Strict).is_err());
assert!(parse("{2=u8}{=u16}", ParserMode::Strict).is_err());
assert!(parse("{2=u8}{1=u16}", ParserMode::Strict).is_err());
}
#[test]
fn range() {
assert_eq!(
parse("{=0..4}", ParserMode::Strict),
Ok(vec![Fragment::Parameter(Parameter {
index: 0,
ty: Type::BitField(0..4),
hint: None,
})])
);
assert_eq!(
parse("{0=30..31}{1=0..4}{1=2..6}", ParserMode::Strict),
Ok(vec![
Fragment::Parameter(Parameter {
index: 0,
ty: Type::BitField(30..31),
hint: None,
}),
Fragment::Parameter(Parameter {
index: 1,
ty: Type::BitField(0..4),
hint: None,
}),
Fragment::Parameter(Parameter {
index: 1,
ty: Type::BitField(2..6),
hint: None,
}),
])
);
assert!(parse("{=0..0}", ParserMode::Strict).is_err());
assert!(parse("{=1..0}", ParserMode::Strict).is_err());
assert!(parse("{=0..129}", ParserMode::Strict).is_err());
assert!(parse("{=128..128}", ParserMode::Strict).is_err());
assert!(parse("{=0..128}", ParserMode::Strict).is_ok());
assert!(parse("{=127..128}", ParserMode::Strict).is_ok());
assert!(parse("{=0..4", ParserMode::Strict).is_err());
assert!(parse("{=0..}", ParserMode::Strict).is_err());
assert!(parse("{=..4}", ParserMode::Strict).is_err());
assert!(parse("{=0.4}", ParserMode::Strict).is_err());
assert!(parse("{=0...4}", ParserMode::Strict).is_err());
}
#[test]
fn arrays() {
assert_eq!(
parse("{=[u8; 0]}", ParserMode::Strict),
Ok(vec![Fragment::Parameter(Parameter {
index: 0,
ty: Type::U8Array(0),
hint: None,
})])
);
assert_eq!(
parse("{=[u8;42]}", ParserMode::Strict),
Ok(vec![Fragment::Parameter(Parameter {
index: 0,
ty: Type::U8Array(42),
hint: None,
})])
);
assert_eq!(
parse("{=[u8; 257]}", ParserMode::Strict),
Ok(vec![Fragment::Parameter(Parameter {
index: 0,
ty: Type::U8Array(257),
hint: None,
})])
);
assert!(parse("{=[u8; \t 3]}", ParserMode::Strict).is_err());
assert!(parse("{=[u8; \n 3]}", ParserMode::Strict).is_err());
assert!(parse("{=[u8; 9999999999999999999999999]}", ParserMode::Strict).is_err());
}
#[test]
fn error_msg() {
assert_eq!(
parse("{=dunno}", ParserMode::Strict),
Err("malformed format string (invalid type specifier `dunno`)".into())
);
assert_eq!(
parse("{dunno}", ParserMode::Strict),
Err("unexpected content \"dunno\" in format string".into())
);
assert_eq!(
parse("{=u8;x}", ParserMode::Strict),
Err("malformed format string (invalid type specifier `u8;x`)".into())
);
assert_eq!(
parse("{dunno=u8:x}", ParserMode::Strict),
Err("unexpected content \"dunno=u8:x\" in format string".into())
);
assert_eq!(
parse("{0dunno}", ParserMode::Strict),
Err("unexpected content \"dunno\" in format string".into())
);
assert_eq!(
parse("{:}", ParserMode::Strict),
Err("malformed format string (missing display hint after ':')".into())
);
}
#[test]
fn brace_escape() {
assert!(parse("}string", ParserMode::Strict).is_err());
assert!(parse("{string", ParserMode::Strict).is_err());
assert!(parse("}", ParserMode::Strict).is_err());
assert!(parse("{", ParserMode::Strict).is_err());
assert_eq!(
parse("}}", ParserMode::Strict),
Ok(vec![Fragment::Literal("}".into())])
);
assert_eq!(
parse("{{", ParserMode::Strict),
Ok(vec![Fragment::Literal("{".into())])
);
assert_eq!(
parse("literal{{literal", ParserMode::Strict),
Ok(vec![Fragment::Literal("literal{literal".into())])
);
assert_eq!(
parse("literal}}literal", ParserMode::Strict),
Ok(vec![Fragment::Literal("literal}literal".into())])
);
assert_eq!(
parse("{{}}", ParserMode::Strict),
Ok(vec![Fragment::Literal("{}".into())])
);
assert_eq!(
parse("}}{{", ParserMode::Strict),
Ok(vec![Fragment::Literal("}{".into())])
);
}
}