Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support truncating or strict-named variants of parsing and formatting #400

Merged
merged 1 commit into from Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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() {}