Repository: bitflags/bitflags
Pull request #400 (2024-03-18 23:17:56)

Support truncating or strict-named variants of parsing and formatting
This PR fills in the missing gaps [from the spec](https://github.com/bitflags/bitflags/blob/main/spec.md#formatting) around parsing/formatting modes. It adds two new variants of `parser::from_str` and `parser::to_writer`:

- `from_str_truncate`/`to_writer_truncate`: Truncates any unknown bits, but still supports parsing and formatting hex values, like `A | 0x2`
- `from_str_strict`/`to_writer_strict`: Only format and parse defined names, ignoring any other bits in formatting, and failing on them in parsing, like `A | B`.


Patch:
diff --git a/src/lib.rs b/src/lib.rs
index d7faaef..f0aed78 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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;
diff --git a/src/parser.rs b/src/parser.rs
index 130dc2e..34b432d 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -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,
@@ -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.
 


Test patch:
diff --git a/src/tests/parser.rs b/src/tests/parser.rs
index b370785..fb27225 100644
--- a/src/tests/parser.rs
+++ b/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
@@ -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::*;
 
@@ -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",
@@ -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
+    }
+}
diff --git a/tests/compile-pass/parser_strict_non_hex_bits.rs b/tests/compile-pass/parser_strict_non_hex_bits.rs
new file mode 100644
index 0000000..3fd1519
--- /dev/null
+++ b/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() {}