Flags, Bitmasks, and Unix File System Permissions in Ruby

One common use of bitwise operators is working with series of flags and options. Using bitwise opterators allows these to be represented by individual bits instead of symbols, integers, or booleans. This makes for memory efficient code and storage.

For example, on unix-like operating systems, file system permissions are managed in three distinct scopes: the user owning the file, the group owning the file, and others.

For each of these scopes, there are three different permissions: read, write, and execute. When written using symbolic notation, the permissions for a specific file might look something like this:

-rwxr-xr-x

Disregarding the first -, which signals that this is a file and not a directory, the notation consists of three triples of flags. The first triple is for the user scope, the second for the group scope, and the last for the others scope. In this particular case, the user is granted all permissions on the file, whereas the group and others can only read and execute it.

These flags are stored internally using the individual bits of an integer according to the following scheme:

rwxr-xr-x
111101101

111101101 binary is equal to 493 decimal or 755 octal.

This might seem overly complex, but it turns out that using bitwise operators, this scheme is surprisingly easy and flexible to work with.

First, let’s define bitmasks for the individual permission bits:

USER_READ      = 0b100000000
USER_WRITE     = 0b010000000
USER_EXECUTE   = 0b001000000
GROUP_READ     = 0b000100000
GROUP_WRITE    = 0b000010000
GROUP_EXECUTE  = 0b000001000
OTHERS_READ    = 0b000000100
OTHERS_WRITE   = 0b000000010
OTHERS_EXECUTE = 0b000000001

The bitmasks have the permission bit they’re representing set to 1 and all other bits set to 0.

To construct permissions from these bitmasks, we can use the bitwise OR operator:

permissions =
  USER_READ | USER_WRITE | GROUP_READ | OTHERS_READ    #=> 420
permissions.to_s(2)    #=> "110100100"
permissions.to_s(8)    #=> "644"

To check whether a specific permission is set we can use the bitwise AND operator and a comparison with 0:

if permissions & USER_READ != 0
  puts "User can read."
end

If we want to test for either one of multiple permissions we can first OR together the permissions we want to test for, and then AND together the resulting bitmask with our permissions:

if permissions & (GROUP_READ | GROUP_WRITE) != 0
  puts "Group can read or write."
end

If we, at some point, decide that we would like to enable further permissions, we can do so using the OR assignment operator:

permissions |= GROUP_WRITE    #=> 436
permissions.to_s(2)           #=> "110110100"
permissions.to_s(8)           #=> "664"

OR-assigning a permission that is already present has no effect.

permissions |= USER_WRITE    #=> 436

To clear out permissions, we can use the AND operator together with a bitmask that has the bits we want to clear set to 0 and all other bits set to 1. To construct this bitmask, we can use the OR operator and the bitwise NOT operator:

GROUP = GROUP_READ | GROUP_WRITE | GROUP_EXECUTE    #=> 56
~GROUP                                              #=> -57
GROUP.to_s(2)                                       #=> "111000"
5.downto(0).map { |n| (~GROUP)[n] }.join            #=> "000111"

Since the result of ~GROUP is a negative integer we can’t get it’s underlying binary representation using Fixnum#to_s. Instead, we need to use Fixnum#[]. Check out Ruby’s Bitwise Operators for an explanation of why.

Now that we have an inverted bitmask, we can use AND-assignment to clear the bits out:

permissions &= ~GROUP    #=> 388
permissions.to_s(2)      #=> "110000100"
permissions.to_s(8)      #=> "604"

To toggle permissions, we can use the bitwise XOR operator. This works equally well for single and multiple bit bitmasks:

group_others_read = GROUP_READ | OTHERS_READ    #=> 36
group_others_read.to_s(2)                       #=> "100100"
permissions ^= group_others_read                #=> 416
permissions.to_s(2)                             #=> "110100000"
permissions.to_s(8)                             #=> "640"

If we want to completely invert the current permissions we can use the NOT operator:

permissions = ~permissions                       #=> -417
binary_string =
  8.downto(0).map { |n| permissions[n] }.join    #=> "001011111"
Integer(binary_string, 2).to_s(8)                #=> "137"

Again, since the resulting integer is negative we have to use Fixnum#[] to get the binary representation. To get the octal representation we have to jump through even more hoops.