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