git_policy_cli/fs_kind.rs
1//! What:
2//! Which kind of filesystem holds a path (btrfs, overlayfs, other), whether a directory is the root of a btrfs
3//! subvolume, and how many bytes a user can still write there.
4//! Why:
5//! Every rung of the cheap-worktree ladder starts from these three facts, which the kernel answers in one
6//! `statfs` call and one inode number, so nothing is spawned: a snapshot needs btrfs and a subvolume root
7//! (inode 256), a reflink works on btrfs and inside an overlay, and everything else keeps Git's own checkout.
8//! `doc/planning/cli-git-btrfs-worktree.md` (section "Degradation ladder") has the measurements behind the ladder.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! const kind = (await statfs(path)).type === 0x9123683e ? 'btrfs' : ...;
13//! const isSubvolumeRoot = kind === 'btrfs' && (await stat(path, { bigint: true })).ino === 256n;
14//! ```
15
16use super::fs_primitive_error::{PrimitiveError, c_path};
17use std::os::unix::fs::MetadataExt;
18use std::path::Path;
19
20/// The inode number of the root directory of every btrfs subvolume (`BTRFS_FIRST_FREE_OBJECTID`,
21/// `include/uapi/linux/btrfs_tree.h`).
22pub const BTRFS_SUBVOLUME_ROOT_INODE: u64 = 256;
23
24/// What:
25/// The filesystem kinds the ladder tells apart.
26/// `#[derive(...)]` generates copying, debug printing and `==`.
27/// Why:
28/// Btrfs allows snapshots and reflinks, overlayfs allows reflinks of files inside it, anything else gets Git's checkout.
29///
30/// In TS you'd write (pseudocode):
31/// ```ts
32/// type FilesystemKind = 'btrfs' | 'overlayfs' | 'other';
33/// ```
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub enum FilesystemKind {
36 /// `BTRFS_SUPER_MAGIC`.
37 Btrfs,
38 /// `OVERLAYFS_SUPER_MAGIC`, the root of a container.
39 Overlayfs,
40 /// Every other `f_type`.
41 Other,
42}
43
44/// What:
45/// Map the `f_type` of a `statfs` answer to a kind.
46/// `u64` is the widest unsigned integer; siblings `u32` and `i64` are what the C field is on some targets.
47/// Why:
48/// The C field is a signed word on some targets and an unsigned one on others, so only its low 32 bits are compared
49/// with the magic numbers, which are all 32-bit values.
50///
51/// In TS you'd write (pseudocode):
52/// ```ts
53/// function classifyMagic(fType: bigint): FilesystemKind { const low = fType & 0xffffffffn; ... }
54/// ```
55pub fn classify_magic(f_type: u64) -> FilesystemKind {
56 let low: u64 = f_type & 0xffff_ffff;
57 if low == (libc::BTRFS_SUPER_MAGIC as u64 & 0xffff_ffff) {
58 return FilesystemKind::Btrfs;
59 }
60 if low == (libc::OVERLAYFS_SUPER_MAGIC as u64 & 0xffff_ffff) {
61 return FilesystemKind::Overlayfs;
62 }
63 return FilesystemKind::Other;
64}
65
66/// What:
67/// The kind of filesystem that holds `path`, and the bytes a user can write there, from one `statfs` call.
68/// Why:
69/// The two answers come from the same call, so a caller that wants both pays once.
70///
71/// In TS you'd write (pseudocode):
72/// ```ts
73/// const { type, bavail, bsize } = await statfs(path);
74/// ```
75pub fn filesystem_facts(path: &Path) -> Result<(FilesystemKind, u64), PrimitiveError> {
76 let name = c_path("statfs", path)?;
77 // SAFETY: `name` is a NUL-terminated string that outlives the call, and `buffer` is a zeroed `statfs` the call fills.
78 let mut buffer: libc::statfs = unsafe { std::mem::zeroed() };
79 let result: libc::c_int = unsafe { libc::statfs(name.as_ptr(), &mut buffer) };
80 if result != 0 {
81 return Err(PrimitiveError::last_os_error("statfs", path));
82 }
83 let free: u64 = (buffer.f_bavail as u64).saturating_mul(buffer.f_bsize as u64);
84 return Ok((classify_magic(buffer.f_type as u64), free));
85}
86
87/// What:
88/// The kind of filesystem that holds `path`.
89/// Why:
90/// Most callers need only the kind.
91///
92/// In TS you'd write (pseudocode):
93/// ```ts
94/// const kind = (await filesystemFacts(path)).kind;
95/// ```
96pub fn filesystem_kind(path: &Path) -> Result<FilesystemKind, PrimitiveError> {
97 return filesystem_facts(path).map(|facts| { return facts.0; });
98}
99
100/// What:
101/// Whether `path` is the root directory of a btrfs subvolume.
102/// Why:
103/// Only a subvolume root can be the source of a snapshot. Inode 256 is the root of every subvolume; on another
104/// filesystem the same inode number means nothing, so the filesystem is checked first and a directory on a
105/// filesystem other than btrfs is never a subvolume root.
106///
107/// In TS you'd write (pseudocode):
108/// ```ts
109/// const isSubvolumeRoot = async (path) => (await filesystemKind(path)) === 'btrfs' && (await stat(path, { bigint: true })).ino === 256n;
110/// ```
111pub fn is_subvolume_root(path: &Path) -> Result<bool, PrimitiveError> {
112 if filesystem_kind(path)? != FilesystemKind::Btrfs {
113 return Ok(false);
114 }
115 let metadata = std::fs::metadata(path).map_err(|error| {
116 return PrimitiveError { operation: "stat", path: path.to_path_buf(), errno: error.raw_os_error().unwrap_or(0) };
117 })?;
118 return Ok(metadata.is_dir() && metadata.ino() == BTRFS_SUBVOLUME_ROOT_INODE);
119}
120
121/// Filesystem kind controls stay out of the release executable.
122#[cfg(test)]
123#[path = "fs_kind_tests.rs"]
124mod tests;