Memra

Asking a file about itself

◈ 10 cards

Retrieve a file metadata with stat, lstat or fstat, mask st_mode to test its type and permissions, and tell a symbolic link from its target in C.

Three ways to ask the same question

ls -l gets its answers from the inode, and so can you. The interface is one structure and three calls that fill it:

#include <sys/stat.h>
int stat(const char *restrict path, struct stat *restrict buf);
int lstat(const char *restrict path, struct stat *restrict buf);
int fstat(int fd, struct stat *restrict buf);

All three return 0 on success and −1 on failure — this is the one place in the file API where the textbook's "Success: 0" happens to be right, and it is right by accident rather than by pattern.

The difference between them is the question they are actually asking:

  • stat(path, &sb) — follow the path to a file and describe that file. If the last component is a symbolic link, stat follows it and describes the target.
  • lstat(path, &sb) — describe whatever the last component is. If it is a symbolic link, you get the link itself: S_ISLNK is true, st_size is the length of the target string.
  • fstat(fd, &sb) — describe the file behind an already-open descriptor. No path lookup, so no race between checking a file and opening it, and no permission question about the path.

The stat/lstat pair is how you answer "hard link or symbolic link?" in C. Run both on the same name: if the answers differ, it is a symlink; a hard link is indistinguishable from the original file because it is the original file, sharing one inode.

stat() needs search (x) permission on every directory in the path but no permission at all on the file itself — it never touches the contents.

What is in the structure, and what conspicuously is not

The fields you will use are st_ino (the inode number), st_mode (type and permissions), st_nlink (how many hard links point here), st_uid and st_gid, st_size (bytes), st_blocks (512-byte blocks actually allocated — the field that gives a sparse file away), and the timestamps st_atim, st_mtim and st_ctim.

Two things are not there, and both are exam bait:

  • The filename. A name lives in a directory entry, which pairs a name with an inode number. This is why a file can have several names (hard links), and why renaming a file touches no inode at all.
  • The file offset. That lives in the system-wide open-file table, one per opening, as the previous lesson established.

And one field is routinely misread: st_ctim is the last attribute change, not creation. A chmod moves it; writing to the file moves both st_mtim and st_ctim; nothing at all records creation time in the portable interface.

Worked example — st_mode, and why you must mask it

st_mode is a single integer carrying three different things at once: the file type, the nine permission bits, and the three special bits (set-user-ID, set-group-ID, sticky). Printing it raw is meaningless. You separate them by masking.

For the type, mask with S_IFMT and compare, or use the macros that do it for you:

struct stat sb;
if (stat(path, &sb) == -1) { perror("stat"); return 1; }

if (S_ISDIR(sb.st_mode))  puts("directory");
else if (S_ISREG(sb.st_mode))  puts("regular file");
else if (S_ISLNK(sb.st_mode))  puts("symbolic link");

S_ISREG, S_ISDIR, S_ISLNK, S_ISCHR, S_ISBLK, S_ISFIFO and S_ISSOCK each take st_mode and are non-zero when true. Each is exactly (m & S_IFMT) == S_IFwhatever behind a macro.

For the mode, mask with 07777 — twelve bits: three special, then user, group and other:

printf("mode %04o\n", sb.st_mode & 07777);

So 0100644 in st_mode is a regular file (0100000) with mode 0644. 0104755 is a regular file with mode 4755 — that leading 4 is set-user-ID, which is why ls -l prints -rwsr-xr-x for /usr/bin/passwd. And 0041777 is a directory with mode 1777: the sticky bit, which is why /tmp prints as drwxrwxrwt.

The existence-and-type check A3 Q8 needs

One concrete use, and it is the first thing the assignment's child process has to do:

struct stat sb;
if (stat(argv[1], &sb) == -1) {
    perror(argv[1]);          /* no such file, or no search permission */
    return 1;
}
if (!S_ISDIR(sb.st_mode)) {
    fprintf(stderr, "%s: not a directory\n", argv[1]);
    return 1;
}

A failing stat and a successful stat on the wrong type are two different errors with two different messages, and separating them is what makes the program usable rather than merely correct.

FieldWhat it holdsWhere it comes fromst_inothe inode numberthe inodest_modetype + 9 permission bits +SUID/SGID/stickythe inodest_nlinkhow many hard links pointat this inodethe inodest_sizelength in bytes (thelogical size)the inodest_blocks512-byte blocks actuallyallocatedthe inodest_ctimlast ATTRIBUTE change, notcreationthe inodethe filenameNOT in struct statthe directory entrythe file offsetNOT in struct statthe open-file tablest_size against st_blocks is how you spot a sparse file: a big logical size on almost no blocks.
The last two rows are the ones the exam asks about: neither the name nor the offset is in the inode.
NORMAL ~/memra/learn/comp-325/stat-the-inode-from-c utf-8 LF