PHP Mode Strings with Stat (and Stream Wrappers)

Feb 15 2012

This article explains how to work with mode strings in stream wrappers, and in low-level calls to PHP's stat() functions. It will be helpful to those writing stream wrappers that must respond to functions like:

  • isdir/isfile
  • file_exists
  • isreadable/iswritable
  • stat

Stream wrappers must supply the right stat arrays for these, and the most confusing value that must be set (for non-C programmers, at least) is the mode string.

I start off with a very basic intro to mode strings, and then dive down into the details of setting mode strings. <!--break-->

A Quick and Dirty Intro to Mode Strings

UNIX users are familiar with mode strings from UNIX file permissions. Using UNIX commands like chmod, one can tell the filesystem what users on the system may read, write, and execute a file.

For example, we can give the owner read and write permissions, members of the UNIX group read permissions, and all others no permissions at all:

$ chmod 640 myfile.test

The above sets the permissions on the file myfile.txt.

Essentially, what we are doing here is writing an octal number where each digit is a type of mask:

  • 0: No permissions
  • 1: Execute
  • 2: Write
  • 4: Read

(There are additional values, but we'll skip for now)

So the mode 421 means...

  • The owner (first digit) can read
  • Group members (second digit) can write
  • All others (third digit) can execute

To build up a string that says "read and write", we add the read and write numbers together: 2 + 4 = 6. Thus, in our original example, where we had 644, this meant "read + write for the owner, and read for the other two catagories".

There are other values (like sticky bits) that you can set on a file mode string, but we're going to skip those for a moment and dive down the rabbit hole.

Stat Mode Strings

The mode strings that come back from stat() have information about the mode string. Here's an example:

<?php
$stats = stat('./myfile.txt');
print $stats['mode'];

// Output: 33184
?>

But wait! Why does that print out 33184, and not 644?

There are two reasons:

  1. More information is added to the mode
  2. The mode is retrieved in decimal format, not octal.

To address the second problem, we can tweak our script just a bit:

<?php
$stats = stat('./myfile.txt');
print decoct($stats['mode']);

// Output: 100640
?>

Running the mode string through decoct (deicimal to octal) will return 100640. See the 640 in there? That's the file permissions. But a second number has been added to it. The permissions number is summed with the file type number.

To get the idea behind this, let's stat a directory with the same file permissions:

<?php
$stats = stat('./mydir');
print decoct($stats['mode']);

// Output: 40640
?>

The last three digits are the same: 640. But where the leading digits were 100, when we stat a directory, they are 40. This is because stat adds numbers based on the file type.

Here are a few important examples (again, these are in octal, not decimal):

  • 0120000: Symbolic link
  • 0100000: File
  • 040000: Directory

A full list of values is available in the documentation for libc's fstat() function.

When you run PHP commands like is_dir() or is_file(), these commands check this particular part of the stat value.

Stream Wrappers and Stat Data

When writing stat() data in stream wrappers, you will need to make sure you set the mode correctly. And this means making sure to set the type data, too. A stream wrapper needs to know what kind of file it is dealing with.

The two functions you will work with are StreamWrapper::url_stat() and StreamWrapper::stream_stat().

As we've seen above, the easiest method of setting the full mode is to add (sum) the permissions bits with the resource type bits. So a directory with global read/write/execute becomes 040000 + 0777 = 040777. Keep in mind that we are doing those computations in octal (thus the leading 0).

Checking Your Work with Masking

PHP provides a number of functions that will help you determine, based on the results of stat(), various pieces of information about your file. Here's rougly how it works.

A mode string is used like a bit mask, which means we can use bitwise logical operators to find out about what values are set.

Here's a full script that you can test with. It shows how masking is used to determine file type, permissions, and so on:

<?php
if (count($argv) &lt; 2) {
  print "Usage: php testmode.php file [file [...]]" . PHP_EOL;
  exit(1);
}

array_shift($argv);

foreach ($argv as $file) {
  print "=== Testing $file" . PHP_EOL;
  $stat = stat($file);
  print "Raw mode: " . $stat['mode'] . PHP_EOL;
  $mode = $stat['mode'];
  print "Octal mode: " . decoct($mode) . PHP_EOL . PHP_EOL;

  print ($mode & 0400) . " is the owner's read bit." . PHP_EOL;
  print ($mode & 0040) . " is the group's read bit." . PHP_EOL;
  print ($mode & 0004) . " is the other's read bit." . PHP_EOL;

  print ($mode & 0200) . " is the owner's write bit." . PHP_EOL;
  print ($mode & 0020) . " is the group's write bit." . PHP_EOL;
  print ($mode & 0002) . " is the other's write bit." . PHP_EOL;

  print PHP_EOL;

  if ($mode & 0100000) {
    print "This is a file." . PHP_EOL;
  }
  if ($mode & 040000) {
    print "This is a directory." . PHP_EOL;
  }

  print PHP_EOL;
}

?>

You probably really want the code above as a Gist.

Mode strings carry along the C/UNIX legacy in PHP, and this isn't a bad thing. The occasional frustrations with this are due more to the fact that the documentation has not been carried on. The underlying system is elegant and efficient. Unfortunately, it's just not readily evident.