File Metadata and Directories
Files are not just data blobs. The kernel stores metadata about every file: its size, owner, permissions, timestamps, and more. This chapter teaches you how to query and modify that metadata, and how to navigate the directory tree from both C and Rust.
The stat() Family
The stat() system call fills a struct stat with information about a file.
There are three variants:
| Function | Operates on | Follows symlinks? |
|---|---|---|
stat() | a path (string) | Yes |
lstat() | a path (string) | No |
fstat() | an open fd (int) | N/A |
/* stat_demo.c -- query file metadata */
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "usage: %s <path>\n", argv[0]);
return 1;
}
struct stat st;
if (stat(argv[1], &st) == -1) {
perror("stat");
return 1;
}
printf("file: %s\n", argv[1]);
printf("inode: %lu\n", (unsigned long)st.st_ino);
printf("size: %ld bytes\n", (long)st.st_size);
printf("blocks: %ld (512-byte units)\n", (long)st.st_blocks);
printf("hard links: %lu\n", (unsigned long)st.st_nlink);
struct passwd *pw = getpwuid(st.st_uid);
struct group *gr = getgrgid(st.st_gid);
printf("owner: %s (uid %d)\n", pw ? pw->pw_name : "?", st.st_uid);
printf("group: %s (gid %d)\n", gr ? gr->gr_name : "?", st.st_gid);
printf("permissions: %o\n", st.st_mode & 07777);
if (S_ISREG(st.st_mode)) printf("type: regular file\n");
else if (S_ISDIR(st.st_mode)) printf("type: directory\n");
else if (S_ISLNK(st.st_mode)) printf("type: symlink\n");
else if (S_ISCHR(st.st_mode)) printf("type: char device\n");
else if (S_ISBLK(st.st_mode)) printf("type: block device\n");
else if (S_ISFIFO(st.st_mode)) printf("type: FIFO/pipe\n");
else if (S_ISSOCK(st.st_mode)) printf("type: socket\n");
char timebuf[64];
struct tm *tm = localtime(&st.st_mtime);
strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm);
printf("modified: %s\n", timebuf);
return 0;
}
$ gcc -Wall -o stat_demo stat_demo.c && ./stat_demo /etc/passwd
file: /etc/passwd
inode: 1048594
size: 2773 bytes
blocks: 8 (512-byte units)
hard links: 1
owner: root (uid 0)
group: root (gid 0)
permissions: 644
type: regular file
modified: 2025-01-15 10:22:33
The struct stat Layout
struct stat
+------------------+--------------------------------------------+
| st_dev | device ID of filesystem |
| st_ino | inode number (unique within filesystem) |
| st_mode | file type + permissions (see below) |
| st_nlink | number of hard links |
| st_uid | owner user ID |
| st_gid | owner group ID |
| st_rdev | device ID (for char/block devices) |
| st_size | file size in bytes |
| st_blksize | optimal I/O block size |
| st_blocks | number of 512-byte blocks allocated |
| st_atim | last access time |
| st_mtim | last modification time |
| st_ctim | last status change time |
+------------------+--------------------------------------------+
st_mode bit layout (16 bits):
+------+------+------+------+------+------+
| type |setuid|setgid|sticky| user | group| other
| 4bit | 1 | 1 | 1 | rwx | rwx | rwx
+------+------+------+------+------+------+
Driver Prep: When you implement a character device driver, the kernel populates some of these fields for you. Your driver's
getattrcallback can override them. Understanding what each field means is essential.
Checking File Type with Macros
The S_IS* macros decode the file type from st_mode:
if (S_ISREG(st.st_mode)) { /* regular file */ }
if (S_ISDIR(st.st_mode)) { /* directory */ }
if (S_ISLNK(st.st_mode)) { /* symbolic link -- use lstat! */ }
if (S_ISCHR(st.st_mode)) { /* character device */ }
if (S_ISBLK(st.st_mode)) { /* block device */ }
if (S_ISFIFO(st.st_mode)) { /* FIFO (named pipe) */ }
if (S_ISSOCK(st.st_mode)) { /* socket */ }
Caution:
stat()follows symbolic links. If you callstat()on a symlink, you get the metadata of the target file. Uselstat()to get the metadata of the symlink itself.
Rust: std::fs::metadata
// metadata_demo.rs -- file metadata in Rust use std::fs; use std::os::unix::fs::MetadataExt; use std::os::unix::fs::PermissionsExt; fn main() -> std::io::Result<()> { let path = "/etc/passwd"; let meta = fs::metadata(path)?; println!("file: {}", path); println!("inode: {}", meta.ino()); println!("size: {} bytes", meta.len()); println!("blocks: {}", meta.blocks()); println!("hard links: {}", meta.nlink()); println!("uid: {}", meta.uid()); println!("gid: {}", meta.gid()); println!("permissions: {:o}", meta.permissions().mode() & 0o7777); if meta.is_file() { println!("type: regular file"); } if meta.is_dir() { println!("type: directory"); } if meta.is_symlink() { println!("type: symlink"); } if let Ok(modified) = meta.modified() { println!("modified: {:?}", modified); } Ok(()) }
For symlink metadata (equivalent to lstat), use fs::symlink_metadata():
#![allow(unused)] fn main() { let meta = fs::symlink_metadata("/some/symlink")?; println!("is symlink: {}", meta.is_symlink()); }
Rust Note:
MetadataExtis Unix-specific (imported fromstd::os::unix::fs). The cross-platformMetadatatype only exposeslen(),is_file(),is_dir(), and timestamps. For inode, uid, gid, and other Unix-specific fields, you need the extension trait.
Changing Permissions and Ownership
/* chmod_demo.c -- change file permissions */
#include <stdio.h>
#include <sys/stat.h>
int main(void)
{
const char *path = "/tmp/chmod_test.txt";
FILE *fp = fopen(path, "w");
if (!fp) { perror("fopen"); return 1; }
fprintf(fp, "secret data\n");
fclose(fp);
if (chmod(path, 0400) == -1) {
perror("chmod");
return 1;
}
struct stat st;
stat(path, &st);
printf("permissions: %o\n", st.st_mode & 07777);
chmod(path, 0644); /* restore */
return 0;
}
For changing ownership, chown(path, uid, gid) works the same way. Only root
(or a process with CAP_CHOWN) can change file ownership to another user.
In Rust:
use std::fs; use std::os::unix::fs::PermissionsExt; fn main() -> std::io::Result<()> { let path = "/tmp/chmod_test_rs.txt"; fs::write(path, "secret data\n")?; let perms = fs::Permissions::from_mode(0o400); fs::set_permissions(path, perms)?; let meta = fs::metadata(path)?; println!("permissions: {:o}", meta.permissions().mode() & 0o7777); fs::set_permissions(path, fs::Permissions::from_mode(0o644))?; Ok(()) }
Directory Operations in C
Reading a Directory
/* readdir_demo.c -- list directory contents */
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/stat.h>
#include <string.h>
int main(int argc, char *argv[])
{
const char *dirpath = argc > 1 ? argv[1] : ".";
DIR *dp = opendir(dirpath);
if (!dp) {
perror("opendir");
return 1;
}
struct dirent *entry;
while ((entry = readdir(dp)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0)
continue;
char fullpath[4096];
snprintf(fullpath, sizeof(fullpath), "%s/%s", dirpath, entry->d_name);
struct stat st;
if (lstat(fullpath, &st) == -1) {
perror(fullpath);
continue;
}
char type = '-';
if (S_ISDIR(st.st_mode)) type = 'd';
else if (S_ISLNK(st.st_mode)) type = 'l';
else if (S_ISCHR(st.st_mode)) type = 'c';
else if (S_ISBLK(st.st_mode)) type = 'b';
printf("%c %8ld %s\n", type, (long)st.st_size, entry->d_name);
}
closedir(dp);
return 0;
}
$ gcc -Wall -o readdir_demo readdir_demo.c && ./readdir_demo /tmp
- 32 fd_demo.txt
- 20 buffered.txt
- 0 chmod_test.txt
Creating and Removing Directories
/* mkdir_rmdir.c -- create and remove directories */
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
int main(void)
{
if (mkdir("/tmp/mydir", 0755) == -1)
perror("mkdir");
if (mkdir("/tmp/mydir/sub", 0755) == -1)
perror("mkdir sub");
printf("created /tmp/mydir/sub\n");
/* rmdir only works on empty directories */
rmdir("/tmp/mydir/sub");
rmdir("/tmp/mydir");
printf("removed both directories\n");
return 0;
}
Caution:
rmdir()fails withENOTEMPTYif the directory is not empty. To remove a directory tree, you must remove its contents first (recursively).
File Operations: unlink, rename
/* unlink_rename.c */
#include <stdio.h>
#include <unistd.h>
int main(void)
{
FILE *fp = fopen("/tmp/old_name.txt", "w");
fprintf(fp, "I will be renamed\n");
fclose(fp);
fp = fopen("/tmp/to_delete.txt", "w");
fprintf(fp, "I will be deleted\n");
fclose(fp);
/* rename() -- atomic on the same filesystem */
if (rename("/tmp/old_name.txt", "/tmp/new_name.txt") == -1) {
perror("rename");
return 1;
}
printf("renamed old_name.txt -> new_name.txt\n");
/* unlink() -- remove a hard link (deletes file if last link) */
if (unlink("/tmp/to_delete.txt") == -1) {
perror("unlink");
return 1;
}
printf("deleted to_delete.txt\n");
unlink("/tmp/new_name.txt");
return 0;
}
Directory Operations in Rust
// readdir_demo.rs -- list directory contents use std::fs; fn main() -> std::io::Result<()> { let dirpath = std::env::args().nth(1).unwrap_or_else(|| ".".to_string()); for entry in fs::read_dir(&dirpath)? { let entry = entry?; let meta = entry.metadata()?; let file_type = entry.file_type()?; let type_char = if file_type.is_dir() { 'd' } else if file_type.is_symlink() { 'l' } else { '-' }; println!("{} {:>8} {}", type_char, meta.len(), entry.file_name().to_string_lossy()); } Ok(()) }
Creating, Removing, Renaming
// dir_ops.rs -- mkdir, rmdir, rename, remove use std::fs; fn main() -> std::io::Result<()> { fs::create_dir_all("/tmp/rustdir/sub")?; println!("created /tmp/rustdir/sub"); fs::write("/tmp/rustdir/sub/hello.txt", "hello\n")?; fs::rename("/tmp/rustdir/sub/hello.txt", "/tmp/rustdir/sub/world.txt")?; println!("renamed hello.txt -> world.txt"); fs::remove_file("/tmp/rustdir/sub/world.txt")?; println!("removed world.txt"); fs::remove_dir_all("/tmp/rustdir")?; println!("removed /tmp/rustdir and all contents"); Ok(()) }
Rust Note:
fs::create_dir_allis likemkdir -p-- it creates parent directories as needed.fs::remove_dir_allis likerm -rf-- it removes everything recursively. In C you must walk the tree yourself.
Symbolic Links and Hard Links
Hard link:
name_a ──> inode 12345 <── name_b
(both names point to the same inode; same data blocks)
Symbolic link:
symlink ──> "path/to/target" (just stores a path string)
|
+-- readlink() returns "path/to/target"
+-- stat() follows to the target
+-- lstat() returns info about the symlink itself
/* links_demo.c -- create hard and symbolic links */
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
int main(void)
{
FILE *fp = fopen("/tmp/original.txt", "w");
fprintf(fp, "original content\n");
fclose(fp);
link("/tmp/original.txt", "/tmp/hardlink.txt");
symlink("/tmp/original.txt", "/tmp/symlink.txt");
struct stat st;
stat("/tmp/original.txt", &st);
printf("original inode: %lu, nlink: %lu\n",
(unsigned long)st.st_ino, (unsigned long)st.st_nlink);
stat("/tmp/hardlink.txt", &st);
printf("hardlink inode: %lu (same!)\n", (unsigned long)st.st_ino);
lstat("/tmp/symlink.txt", &st);
printf("symlink inode: %lu (different)\n", (unsigned long)st.st_ino);
char target[256];
ssize_t n = readlink("/tmp/symlink.txt", target, sizeof(target) - 1);
if (n != -1) {
target[n] = '\0';
printf("symlink target: %s\n", target);
}
unlink("/tmp/hardlink.txt");
unlink("/tmp/symlink.txt");
unlink("/tmp/original.txt");
return 0;
}
$ gcc -Wall -o links_demo links_demo.c && ./links_demo
original inode: 2359301, nlink: 2
hardlink inode: 2359301 (same!)
symlink inode: 2359447 (different)
symlink target: /tmp/original.txt
In Rust:
// links_demo.rs use std::fs; use std::os::unix::fs as unix_fs; use std::os::unix::fs::MetadataExt; fn main() -> std::io::Result<()> { fs::write("/tmp/original_rs.txt", "original content\n")?; fs::hard_link("/tmp/original_rs.txt", "/tmp/hardlink_rs.txt")?; unix_fs::symlink("/tmp/original_rs.txt", "/tmp/symlink_rs.txt")?; let orig = fs::metadata("/tmp/original_rs.txt")?; let hard = fs::metadata("/tmp/hardlink_rs.txt")?; let sym = fs::symlink_metadata("/tmp/symlink_rs.txt")?; println!("original inode: {}, nlink: {}", orig.ino(), orig.nlink()); println!("hardlink inode: {} (same!)", hard.ino()); println!("symlink inode: {} (different)", sym.ino()); let target = fs::read_link("/tmp/symlink_rs.txt")?; println!("symlink target: {}", target.display()); fs::remove_file("/tmp/hardlink_rs.txt")?; fs::remove_file("/tmp/symlink_rs.txt")?; fs::remove_file("/tmp/original_rs.txt")?; Ok(()) }
Working with Paths in Rust
Rust provides Path and PathBuf for safe path manipulation:
// path_demo.rs use std::path::{Path, PathBuf}; fn main() { let p = Path::new("/home/user/documents/report.txt"); println!("file name: {:?}", p.file_name()); println!("stem: {:?}", p.file_stem()); println!("extension: {:?}", p.extension()); println!("parent: {:?}", p.parent()); println!("is absolute: {}", p.is_absolute()); let mut pb = PathBuf::from("/home/user"); pb.push("documents"); pb.push("report.txt"); println!("built path: {}", pb.display()); let full = Path::new("/var/log").join("syslog"); println!("joined: {}", full.display()); }
$ rustc path_demo.rs && ./path_demo
file name: Some("report.txt")
stem: Some("report")
extension: Some("txt")
parent: Some("/home/user/documents")
is absolute: true
built path: /home/user/documents/report.txt
joined: /var/log/syslog
Try It: Write a program (C or Rust) that recursively walks a directory tree, printing each file's path and size. In C, you will need a recursive function that calls
opendir/readdir/stat. In Rust, consider writing a recursive function or use thewalkdircrate.
Quick Knowledge Check
-
What is the difference between
stat()andlstat()when called on a symbolic link? -
What does
st_nlinkequal for a regular file with no extra hard links? -
Why does
rmdir()fail on a non-empty directory, and what must you do to remove a full directory tree in C?
Common Pitfalls
-
Using
stat()on symlinks when you meantlstat(). You silently get the target's metadata. -
Buffer overflow in path construction. In C, building paths with
sprintfwithout length checks is a classic vulnerability. Usesnprintf. -
TOCTOU races. Checking a file's existence with
stat()and then opening it is a race condition. Another process can change the file between your check and your open. UseO_CREAT | O_EXCLfor atomic creation. -
Forgetting
closedir(). Leaks a file descriptor just like forgettingclose(). -
Assuming
d_typeinstruct dirent. Not all filesystems populated_type. Always fall back tostat()ifd_type == DT_UNKNOWN. -
Hard-linking across filesystems. Hard links only work within a single filesystem. Use symbolic links for cross-filesystem references.