Skip to content

Commit

Permalink
Merge pull request #400 from KodrAus/feat/parser-modes
Browse files Browse the repository at this point in the history
Support truncating or strict-named variants of parsing and formatting
  • Loading branch information
KodrAus committed Mar 19, 2024
2 parents e2c8409 + 6eaed0b commit 6965e84
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 5 deletions.
3 changes: 2 additions & 1 deletion src/lib.rs
Expand Up @@ -252,7 +252,8 @@ mod traits;

#[doc(hidden)]
pub mod __private {
#[allow(unused_imports)] // Easier than conditionally checking any optional external dependencies
#[allow(unused_imports)]
// Easier than conditionally checking any optional external dependencies
pub use crate::{external::__private::*, traits::__private::*};

pub use core;
Expand Down
85 changes: 85 additions & 0 deletions src/parser.rs
Expand Up @@ -77,8 +77,10 @@ where
fmt::Result::Ok(())
}

#[cfg(feature = "serde")]
pub(crate) struct AsDisplay<'a, B>(pub(crate) &'a B);

#[cfg(feature = "serde")]
impl<'a, B: Flags> fmt::Display for AsDisplay<'a, B>
where
B::Bits: WriteHex,
Expand Down Expand Up @@ -134,6 +136,89 @@ where
Ok(parsed_flags)
}

/**
Write a flags value as text, ignoring any unknown bits.
*/
pub fn to_writer_truncate<B: Flags>(flags: &B, writer: impl Write) -> Result<(), fmt::Error>
where
B::Bits: WriteHex,
{
to_writer(&B::from_bits_truncate(flags.bits()), writer)
}

/**
Parse a flags value from text.
This function will fail on any names that don't correspond to defined flags.
Unknown bits will be ignored.
*/
pub fn from_str_truncate<B: Flags>(input: &str) -> Result<B, ParseError>
where
B::Bits: ParseHex,
{
Ok(B::from_bits_truncate(from_str::<B>(input)?.bits()))
}

/**
Write only the contained, defined, named flags in a flags value as text.
*/
pub fn to_writer_strict<B: Flags>(flags: &B, mut writer: impl Write) -> Result<(), fmt::Error> {
// This is a simplified version of `to_writer` that ignores
// any bits not corresponding to a named flag

let mut first = true;
let mut iter = flags.iter_names();
for (name, _) in &mut iter {
if !first {
writer.write_str(" | ")?;
}

first = false;
writer.write_str(name)?;
}

fmt::Result::Ok(())
}

/**
Parse a flags value from text.
This function will fail on any names that don't correspond to defined flags.
This function will fail to parse hex values.
*/
pub fn from_str_strict<B: Flags>(input: &str) -> Result<B, ParseError> {
// This is a simplified version of `from_str` that ignores
// any bits not corresponding to a named flag

let mut parsed_flags = B::empty();

// If the input is empty then return an empty set of flags
if input.trim().is_empty() {
return Ok(parsed_flags);
}

for flag in input.split('|') {
let flag = flag.trim();

// If the flag is empty then we've got missing input
if flag.is_empty() {
return Err(ParseError::empty_flag());
}

// If the flag starts with `0x` then it's a hex number
// These aren't supported in the strict parser
if flag.starts_with("0x") {
return Err(ParseError::invalid_hex_flag("unsupported hex flag value"));
}

let parsed_flag = B::from_name(flag).ok_or_else(|| ParseError::invalid_named_flag(flag))?;

parsed_flags.insert(parsed_flag);
}

Ok(parsed_flags)
}

/**
Encode a value as a hex string.
Expand Down
224 changes: 220 additions & 4 deletions src/tests/parser.rs
@@ -1,9 +1,6 @@
use super::*;

use crate::{
parser::{from_str, to_writer},
Flags,
};
use crate::{parser::*, Flags};

#[test]
#[cfg(not(miri))] // Very slow in miri
Expand All @@ -22,6 +19,51 @@ fn roundtrip() {
}
}

#[test]
#[cfg(not(miri))] // Very slow in miri
fn roundtrip_truncate() {
let mut s = String::new();

for a in 0u8..=255 {
for b in 0u8..=255 {
let f = TestFlags::from_bits_retain(a | b);

s.clear();
to_writer_truncate(&f, &mut s).unwrap();

assert_eq!(
TestFlags::from_bits_truncate(f.bits()),
from_str_truncate::<TestFlags>(&s).unwrap()
);
}
}
}

#[test]
#[cfg(not(miri))] // Very slow in miri
fn roundtrip_strict() {
let mut s = String::new();

for a in 0u8..=255 {
for b in 0u8..=255 {
let f = TestFlags::from_bits_retain(a | b);

s.clear();
to_writer_strict(&f, &mut s).unwrap();

let mut strict = TestFlags::empty();
for (_, flag) in f.iter_names() {
strict |= flag;
}
let f = strict;

if let Ok(s) = from_str_strict::<TestFlags>(&s) {
assert_eq!(f, s);
}
}
}
}

mod from_str {
use super::*;

Expand Down Expand Up @@ -97,6 +139,8 @@ mod to_writer {

assert_eq!("ABC", write(TestFlagsInvert::all()));

assert_eq!("0x1", write(TestOverlapping::from_bits_retain(1)));

assert_eq!("A", write(TestOverlappingFull::C));
assert_eq!(
"A | D",
Expand All @@ -114,3 +158,175 @@ mod to_writer {
s
}
}

mod from_str_truncate {
use super::*;

#[test]
fn valid() {
assert_eq!(0, from_str_truncate::<TestFlags>("").unwrap().bits());

assert_eq!(1, from_str_truncate::<TestFlags>("A").unwrap().bits());
assert_eq!(1, from_str_truncate::<TestFlags>(" A ").unwrap().bits());
assert_eq!(
1 | 1 << 1 | 1 << 2,
from_str_truncate::<TestFlags>("A | B | C").unwrap().bits()
);
assert_eq!(
1 | 1 << 1 | 1 << 2,
from_str_truncate::<TestFlags>("A\n|\tB\r\n| C ")
.unwrap()
.bits()
);
assert_eq!(
1 | 1 << 1 | 1 << 2,
from_str_truncate::<TestFlags>("A|B|C").unwrap().bits()
);

assert_eq!(0, from_str_truncate::<TestFlags>("0x8").unwrap().bits());
assert_eq!(1, from_str_truncate::<TestFlags>("A | 0x8").unwrap().bits());
assert_eq!(
1 | 1 << 1,
from_str_truncate::<TestFlags>("0x1 | 0x8 | B")
.unwrap()
.bits()
);

assert_eq!(
1 | 1 << 1,
from_str_truncate::<TestUnicode>("一 | 二").unwrap().bits()
);
}
}

mod to_writer_truncate {
use super::*;

#[test]
fn cases() {
assert_eq!("", write(TestFlags::empty()));
assert_eq!("A", write(TestFlags::A));
assert_eq!("A | B | C", write(TestFlags::all()));
assert_eq!("", write(TestFlags::from_bits_retain(1 << 3)));
assert_eq!(
"A",
write(TestFlags::A | TestFlags::from_bits_retain(1 << 3))
);

assert_eq!("", write(TestZero::ZERO));

assert_eq!("ABC", write(TestFlagsInvert::all()));

assert_eq!("0x1", write(TestOverlapping::from_bits_retain(1)));

assert_eq!("A", write(TestOverlappingFull::C));
assert_eq!(
"A | D",
write(TestOverlappingFull::C | TestOverlappingFull::D)
);
}

fn write<F: Flags>(value: F) -> String
where
F::Bits: crate::parser::WriteHex,
{
let mut s = String::new();

to_writer_truncate(&value, &mut s).unwrap();
s
}
}

mod from_str_strict {
use super::*;

#[test]
fn valid() {
assert_eq!(0, from_str_strict::<TestFlags>("").unwrap().bits());

assert_eq!(1, from_str_strict::<TestFlags>("A").unwrap().bits());
assert_eq!(1, from_str_strict::<TestFlags>(" A ").unwrap().bits());
assert_eq!(
1 | 1 << 1 | 1 << 2,
from_str_strict::<TestFlags>("A | B | C").unwrap().bits()
);
assert_eq!(
1 | 1 << 1 | 1 << 2,
from_str_strict::<TestFlags>("A\n|\tB\r\n| C ")
.unwrap()
.bits()
);
assert_eq!(
1 | 1 << 1 | 1 << 2,
from_str_strict::<TestFlags>("A|B|C").unwrap().bits()
);

assert_eq!(
1 | 1 << 1,
from_str_strict::<TestUnicode>("一 | 二").unwrap().bits()
);
}

#[test]
fn invalid() {
assert!(from_str_strict::<TestFlags>("a")
.unwrap_err()
.to_string()
.starts_with("unrecognized named flag"));
assert!(from_str_strict::<TestFlags>("A & B")
.unwrap_err()
.to_string()
.starts_with("unrecognized named flag"));

assert!(from_str_strict::<TestFlags>("0x1")
.unwrap_err()
.to_string()
.starts_with("invalid hex flag"));
assert!(from_str_strict::<TestFlags>("0xg")
.unwrap_err()
.to_string()
.starts_with("invalid hex flag"));
assert!(from_str_strict::<TestFlags>("0xffffffffffff")
.unwrap_err()
.to_string()
.starts_with("invalid hex flag"));
}
}

mod to_writer_strict {
use super::*;

#[test]
fn cases() {
assert_eq!("", write(TestFlags::empty()));
assert_eq!("A", write(TestFlags::A));
assert_eq!("A | B | C", write(TestFlags::all()));
assert_eq!("", write(TestFlags::from_bits_retain(1 << 3)));
assert_eq!(
"A",
write(TestFlags::A | TestFlags::from_bits_retain(1 << 3))
);

assert_eq!("", write(TestZero::ZERO));

assert_eq!("ABC", write(TestFlagsInvert::all()));

assert_eq!("", write(TestOverlapping::from_bits_retain(1)));

assert_eq!("A", write(TestOverlappingFull::C));
assert_eq!(
"A | D",
write(TestOverlappingFull::C | TestOverlappingFull::D)
);
}

fn write<F: Flags>(value: F) -> String
where
F::Bits: crate::parser::WriteHex,
{
let mut s = String::new();

to_writer_strict(&value, &mut s).unwrap();
s
}
}
11 changes: 11 additions & 0 deletions tests/compile-pass/parser_strict_non_hex_bits.rs
@@ -0,0 +1,11 @@
// NOTE: Missing `B::Bits: WriteHex/ParseHex`

pub fn format<B: bitflags::Flags>(flags: B) {
let _ = bitflags::parser::to_writer_strict(&flags, String::new());
}

pub fn parse<B: bitflags::Flags>(input: &str) {
let _ = bitflags::parser::from_str_strict::<B>(input);
}

fn main() {}

0 comments on commit 6965e84

Please sign in to comment.