May 03, 2025
3 min read
Rust,

Using Bit Masks for Conditional Checks in Rust

In Rust, using bit masks (or bit flags) for conditional checks is a classic and efficient way to handle multiple state flags, especially suitable for scenarios requiring high performance and low memory usage.

The Linux file permission system (rwx) is also based on bit masking.

Let’s first understand the basic bitwise operations:

OperationRust SyntaxDescriptionExample
AND&Keep bits that are 1 in both0b1010 & 0b1100 = 0b1000
OR|Set bit to 1 if any side is 10b1010 | 0b1100 = 0b1110
XOR^Set bit to 1 if bits differ0b1010 ^ 0b1100 = 0b0110
NOT!Invert all bits!0b1010 = 0b0101 (depends on bit width)
Left Shift<<Shift left, fill lower bits with 00b0001 << 3 = 0b1000
Right Shift>>Shift right, fill higher bits with sign bit (arithmetic shift)0b1000_0000 >> 3 = 0b1111_0000 (for type i8)
A byte has 8 bits, each of which can represent a boolean state — e.g., 0 means off, and 1 means on.

We’ll use a permission system as an example and define a struct:

#[derive(Debug, Clone, Copy)]
pub struct Permissions(u8);

impl Permissions { 
    const READ: u8    = 0b0000_0001; // 1 << 0
    const WRITE: u8   = 0b0000_0010; // 1 << 1
    const EXECUTE: u8 = 0b0000_0100; // 1 << 2
    const DELETE: u8  = 0b0000_1000; // 1 << 3
}

We use the last four bits to represent four permissions: read, write, execute, and delete.

Suppose there is a permission value of 3, whose binary representation is 0b0000_0011. We can tell this permission includes read and write because it is formed by combining 0b0000_0001 | 0b0000_0010.

We can check whether a given value contains a specific permission like this:

    // Assume this is the permission we're checking
    let mut perms = Permissions(Permissions::READ | Permissions::WRITE);
    // Or alternatively: let mut perms = 3;

	// Check if perms has read permission
    println!("{:?}", (perms.0 & Permissions::READ) != 0); // true
    // Check if perms has write permission
    println!("{:?}", (perms.0 & Permissions::WRITE) != 0); // true
    // Check if perms has both read and write permissions
    println!("{:?}", (perms.0 & (Permissions::READ | Permissions::WRITE)) 
        == (Permissions::READ | Permissions::WRITE)); // true
    // Check if perms has delete permission
    println!("{:?}", (perms.0 & Permissions::DELETE) != 0);

Because 0 is represented in binary as 0b00000000, it directly means no permissions. Taking read permission as an example, using the & bitwise operation (result is 1 only where both sides are 1):

0b0000_0011 --> The value being checked (e.g., 3)
0b0000_0001 --> Read permission flag
          & --> Bitwise AND, keeps bits where both are 1
===========
0b0000_0001 --> Result is non-zero → has read permission

When checking combined permissions, we take advantage of the | (OR) operator, which retains all bits set to 1. For example, when checking for both read and write permissions using Permissions::READ | Permissions::WRITE:

0b0000_0001
0b0000_0010
          |
===========
0b0000_0011 

Checking other permissions follows the same logic.

bitflags

Interestingly, someone created a more semantically expressive and user-friendly API based on this concept — the bitflags crate.

cargo add bitflags

This crate enables more complex but semantically simpler code. Here’s an official example:

bitflags! {
	/// Represents a set of flags.
	#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
	struct Flags: u32 {
	    /// Flag `A`, located at bit 0.
	    const A = 0b00000001;

	    /// Flag `B`, located at bit 1.
	    const B = 0b00000010;

	    /// Flag `C`, located at bit 2.
	    const C = 0b00000100;

	    /// Combination of `A`, `B`, and `C`.
	    const ABC = Self::A.bits() | Self::B.bits() | Self::C.bits();
	}
}

fn main() {
	let e1 = Flags::A | Flags::C;  // Set containing A and C
	let e2 = Flags::B | Flags::C;  // Set containing B and C

	assert_eq!((e1 | e2), Flags::ABC);   // Union → contains A, B, C
	assert_eq!((e1 & e2), Flags::C);     // Intersection → only C
	assert_eq!((e1 - e2), Flags::A);     // Difference → only A
	assert_eq!(!e2, Flags::A); // Complement → bits not in e2 (in this case, A)
}