Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ mod strategy;

#[cfg(all(test, target_os = "linux"))]
mod linux_filesystem_tests;
#[cfg(all(test, target_os = "linux"))]
mod test_support;

use id::RiftId;
use name::RiftName;
Expand Down
36 changes: 36 additions & 0 deletions crates/core/src/linux_filesystem_tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::test_support::linux_extents::{assert_shared_extents_when_reliable, is_btrfs_subvolume};
use crate::{Create, Error, InitOutcome, Manager};
use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
Expand All @@ -24,6 +25,7 @@ fn production_supported_linux_filesystem_round_trip() {
InitOutcome::AlreadyInitialized
);
assert!(source.join(".rift").exists());
assert_btrfs_subvolume_if_required(&source);

let child = manager
.create(Create {
Expand All @@ -32,6 +34,7 @@ fn production_supported_linux_filesystem_round_trip() {
into: None,
})
.unwrap();
assert_btrfs_subvolume_if_required(&child);
assert_rich_copy(&child);
assert_detached_git_copy(&source, &child);

Expand All @@ -44,7 +47,13 @@ fn production_supported_linux_filesystem_round_trip() {
})
.unwrap();
assert_eq!(custom, custom_parent.join("custom"));
assert_btrfs_subvolume_if_required(&custom);
assert_rich_copy(&custom);
assert_native_cow_copy(
&source.join("nested/deeper/leaf.txt"),
&child.join("nested/deeper/leaf.txt"),
);
assert_native_cow_copy(&source.join("untracked.txt"), &custom.join("untracked.txt"));

let grandchild = manager
.create(Create {
Expand Down Expand Up @@ -230,6 +239,33 @@ fn assert_reflink_probe_cleaned_up(path: &Path) {
);
}

fn assert_btrfs_subvolume_if_required(path: &Path) {
if std::env::var_os("RIFT_REQUIRE_BTRFS_TESTS").is_some() {
assert!(
is_btrfs_subvolume(path).unwrap(),
"{} should be a btrfs subvolume",
path.display()
);
}
}

fn assert_native_cow_copy(source: &Path, child: &Path) {
if std::env::var_os("RIFT_REQUIRE_REFLINK_TESTS").is_some() {
assert_shared_extents_when_reliable(source, child);
}
assert_copy_diverges_after_mutation(source, child);
}

fn assert_copy_diverges_after_mutation(source: &Path, child: &Path) {
let original = fs::read_to_string(source).unwrap();
assert_eq!(fs::read_to_string(child).unwrap(), original);
fs::write(source, "parent mutation").unwrap();
assert_eq!(fs::read_to_string(child).unwrap(), original);
fs::write(child, "child mutation").unwrap();
assert_eq!(fs::read_to_string(source).unwrap(), "parent mutation");
assert_eq!(fs::read_to_string(child).unwrap(), "child mutation");
}

fn same_device(left: &Path, right: &Path) -> bool {
fs::metadata(left).unwrap().dev() == fs::metadata(right).unwrap().dev()
}
Expand Down
24 changes: 22 additions & 2 deletions crates/core/src/strategy/btrfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,16 +382,26 @@ mod linux_tests {
};
let source = temp.path().join("source");
let snapshot = temp.path().join("snapshot");
let filtered = temp.path().join("filtered");
create_btrfs_subvolume(&source).unwrap();
fs::write(source.join("file.txt"), "hello").unwrap();
fs::write(source.join("file.txt"), "shared before mutation").unwrap();

copy_directory_linux(&source, &snapshot, CopyMode::All).unwrap();
assert!(is_btrfs_subvolume(&snapshot).unwrap());
assert_eq!(
fs::read_to_string(snapshot.join("file.txt")).unwrap(),
"hello"
"shared before mutation"
);
assert_copy_diverges_after_mutation(&source.join("file.txt"), &snapshot.join("file.txt"));

copy_directory_linux(&source, &filtered, CopyMode::Filtered).unwrap();
assert!(is_btrfs_subvolume(&filtered).unwrap());
assert_copy_diverges_after_mutation(&source.join("file.txt"), &filtered.join("file.txt"));

remove_directory_linux(&filtered).unwrap();
remove_directory_linux(&snapshot).unwrap();
remove_directory_linux(&source).unwrap();
assert!(!filtered.exists());
assert!(!snapshot.exists());
assert!(!source.exists());
}
Expand Down Expand Up @@ -438,4 +448,14 @@ mod linux_tests {
remove_emptyable_subvolume(&tree).unwrap();
assert!(!tree.exists());
}

fn assert_copy_diverges_after_mutation(source: &Path, clone: &Path) {
let original = fs::read_to_string(source).unwrap();
assert_eq!(fs::read_to_string(clone).unwrap(), original);
fs::write(source, "parent mutation").unwrap();
assert_eq!(fs::read_to_string(clone).unwrap(), original);
fs::write(clone, "child mutation").unwrap();
assert_eq!(fs::read_to_string(source).unwrap(), "parent mutation");
assert_eq!(fs::read_to_string(clone).unwrap(), "child mutation");
}
}
15 changes: 15 additions & 0 deletions crates/core/src/strategy/reflink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ pub(super) fn c_path(path: &Path) -> Result<std::ffi::CString> {
mod tests {
use super::*;
use crate::strategy::linux::{Filesystem, LinuxStrategy, filesystem};
use crate::test_support::linux_extents::assert_shared_extents_when_reliable;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use tempfile::{Builder, TempDir};

Expand Down Expand Up @@ -350,7 +351,9 @@ mod tests {
fs::set_permissions(&source, fs::Permissions::from_mode(0o750)).unwrap();
fs::create_dir(&nested).unwrap();
let file = nested.join("file.txt");
let cow = nested.join("cow.txt");
fs::write(&file, "hello").unwrap();
fs::write(&cow, "shared before mutation").unwrap();
fs::set_permissions(&file, fs::Permissions::from_mode(0o640)).unwrap();
fs::hard_link(&file, nested.join("hard.txt")).unwrap();
std::os::unix::fs::symlink("file.txt", nested.join("link.txt")).unwrap();
Expand Down Expand Up @@ -387,6 +390,8 @@ mod tests {
fs::metadata(&destination).unwrap().permissions().mode() & 0o777,
0o750
);
assert_shared_extents_when_reliable(&cow, &destination.join("nested/cow.txt"));
assert_copy_diverges_after_mutation(&cow, &destination.join("nested/cow.txt"));
LinuxStrategy.remove_directory(&destination).unwrap();
assert!(!destination.exists());
}
Expand Down Expand Up @@ -424,4 +429,14 @@ mod tests {
));
assert!(!destination.exists());
}

fn assert_copy_diverges_after_mutation(source: &Path, clone: &Path) {
let original = fs::read_to_string(source).unwrap();
assert_eq!(fs::read_to_string(clone).unwrap(), original);
fs::write(source, "parent mutation").unwrap();
assert_eq!(fs::read_to_string(clone).unwrap(), original);
fs::write(clone, "child mutation").unwrap();
assert_eq!(fs::read_to_string(source).unwrap(), "parent mutation");
assert_eq!(fs::read_to_string(clone).unwrap(), "child mutation");
}
}
154 changes: 154 additions & 0 deletions crates/core/src/test_support/linux_extents.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use crate::{Error, Result};
use std::fs::File;
use std::os::fd::AsRawFd;
use std::path::Path;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum LinuxFilesystem {
Btrfs,
Xfs,
Zfs,
Other,
}

pub(crate) fn filesystem(path: &Path) -> Result<LinuxFilesystem> {
use std::os::unix::ffi::OsStrExt;

const BTRFS_SUPER_MAGIC: libc::c_long = 0x9123_683e;
const XFS_SUPER_MAGIC: libc::c_long = 0x5846_5342;
const ZFS_SUPER_MAGIC: libc::c_long = 0x2fc1_2fc1;

let path = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|_| Error::Path(format!("path contains a null byte: {}", path.display())))?;
// SAFETY: `statfs` is a plain C struct that the kernel fully initializes.
let mut stat: libc::statfs = unsafe { std::mem::zeroed() };
// SAFETY: `path` is a valid C string, and `stat` points to writable memory.
if unsafe { libc::statfs(path.as_ptr(), &mut stat) } != 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(match stat.f_type {
BTRFS_SUPER_MAGIC => LinuxFilesystem::Btrfs,
XFS_SUPER_MAGIC => LinuxFilesystem::Xfs,
ZFS_SUPER_MAGIC => LinuxFilesystem::Zfs,
_ => LinuxFilesystem::Other,
})
}

pub(crate) fn is_btrfs_subvolume(path: &Path) -> Result<bool> {
use std::os::unix::fs::MetadataExt;

Ok(filesystem(path)? == LinuxFilesystem::Btrfs && std::fs::metadata(path)?.ino() == 256)
}

pub(crate) fn assert_shared_extents_when_reliable(source: &Path, clone: &Path) {
match (
filesystem(source).unwrap(),
have_shared_extents(source, clone),
) {
(_, Ok(true)) => {}
(LinuxFilesystem::Xfs, Ok(false)) => {
panic!(
"expected FIEMAP shared extents for XFS reflink copy: {} -> {}",
source.display(),
clone.display()
);
}
(LinuxFilesystem::Xfs, Err(error)) => {
panic!(
"expected readable FIEMAP shared extents for XFS reflink copy: {} -> {}: {error}",
source.display(),
clone.display()
);
}
(_, Ok(false) | Err(_)) => {}
}
}

fn have_shared_extents(source: &Path, clone: &Path) -> std::io::Result<bool> {
let source = file_extents(source)?;
let clone = file_extents(clone)?;

Ok(source.iter().any(|source| {
source.is_shared()
&& clone
.iter()
.any(|clone| clone.is_shared() && source.physical_range_overlaps(clone))
}))
}

fn file_extents(path: &Path) -> std::io::Result<Vec<FiemapExtent>> {
const FS_IOC_FIEMAP: libc::c_ulong = 0xc020_660b;
const FIEMAP_FLAG_SYNC: u32 = 0x0000_0001;
const EXTENT_COUNT: u32 = 32;

let file = File::open(path)?;
let mut request = FiemapRequest {
map: Fiemap {
fm_start: 0,
fm_length: u64::MAX,
fm_flags: FIEMAP_FLAG_SYNC,
fm_mapped_extents: 0,
fm_extent_count: EXTENT_COUNT,
fm_reserved: 0,
},
extents: [FiemapExtent::default(); EXTENT_COUNT as usize],
};

// SAFETY: `file` is open for the duration of the call, and `request`
// matches the Linux FIEMAP layout with room for `fm_extent_count` extents.
if unsafe { libc::ioctl(file.as_raw_fd(), FS_IOC_FIEMAP, &mut request) } != 0 {
return Err(std::io::Error::last_os_error());
}

let mapped_extents = request.map.fm_mapped_extents.min(EXTENT_COUNT) as usize;
Ok(request.extents[..mapped_extents].to_vec())
}

#[repr(C)]
struct Fiemap {
fm_start: u64,
fm_length: u64,
fm_flags: u32,
fm_mapped_extents: u32,
fm_extent_count: u32,
fm_reserved: u32,
}

#[repr(C)]
struct FiemapRequest {
map: Fiemap,
extents: [FiemapExtent; 32],
}

#[derive(Clone, Copy, Default)]
#[repr(C)]
struct FiemapExtent {
fe_logical: u64,
fe_physical: u64,
fe_length: u64,
fe_reserved64: [u64; 2],
fe_flags: u32,
fe_reserved: [u32; 3],
}

impl FiemapExtent {
fn is_shared(self) -> bool {
const FIEMAP_EXTENT_SHARED: u32 = 0x0000_2000;

self.fe_flags & FIEMAP_EXTENT_SHARED != 0
}

fn physical_range_overlaps(self, other: &Self) -> bool {
let Some(end) = self.fe_physical.checked_add(self.fe_length) else {
return false;
};
let Some(other_end) = other.fe_physical.checked_add(other.fe_length) else {
return false;
};

self.fe_length > 0
&& other.fe_length > 0
&& self.fe_physical < other_end
&& other.fe_physical < end
}
}
1 change: 1 addition & 0 deletions crates/core/src/test_support/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) mod linux_extents;
Loading