use super::fs_clone_exchange::exchange_paths;
use super::fs_kind::FilesystemKind;
use super::fs_primitive_error::PrimitiveError;
use super::subvolume_arguments::{MigrateArguments, MigrateMode};
use super::subvolume_command::execute;
use super::subvolume_error::Interrupt;
use super::subvolume_platform::Platform;
use crate::test_support::{fixture, git, git_text, repository};
use std::collections::HashSet;
use std::fs::File;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
pub(crate) struct PlainPlatform {
pub subvolumes: Mutex<HashSet<(u64, u64)>>,
pub kind: FilesystemKind,
pub free: Option<u64>,
pub removal: bool,
pub real_clone: bool,
pub mount_point: bool,
}
impl PlainPlatform {
pub(crate) fn new() -> PlainPlatform {
return PlainPlatform {
subvolumes: Mutex::new(HashSet::new()),
kind: FilesystemKind::Btrfs,
free: Some(u64::MAX / 2),
removal: true,
real_clone: false,
mount_point: false,
};
}
pub(crate) fn register(&self, path: &Path) {
let metadata: std::fs::Metadata = std::fs::metadata(path).expect("registered path exists");
self.subvolumes.lock().expect("the registry of subvolumes").insert((metadata.dev(), metadata.ino()));
}
}
impl Platform for PlainPlatform {
fn conversion_environment(&self) -> super::worktree_clean_content::ConversionEnvironment {
return super::worktree_clean_content::ConversionEnvironment {
scope: super::repo_read_config_scan::ScopeEnvironment { no_system: true, ..super::repo_read_config_scan::ScopeEnvironment::default() },
variables: Vec::new(),
system_attributes: super::worktree_clean_content::SystemAttributes::Disabled,
alternate_attribute_source: None,
};
}
fn filesystem_facts(&self, path: &Path) -> Result<(FilesystemKind, u64), PrimitiveError> {
let (_, real_free) = super::fs_kind::filesystem_facts(path)?;
return Ok((self.kind, self.free.unwrap_or(real_free)));
}
fn is_subvolume_root(&self, path: &Path) -> Result<bool, PrimitiveError> {
let Ok(metadata) = std::fs::metadata(path) else { return Ok(false) };
return Ok(self.subvolumes.lock().expect("the registry of subvolumes").contains(&(metadata.dev(), metadata.ino())));
}
fn create_subvolume(&self, path: &Path) -> Result<(), PrimitiveError> {
std::fs::create_dir(path).map_err(|error: std::io::Error| {
return PrimitiveError { operation: "BTRFS_IOC_SUBVOL_CREATE", path: path.to_path_buf(), errno: error.raw_os_error().unwrap_or(0) };
})?;
self.register(path);
return Ok(());
}
fn destroy_subvolume(&self, path: &Path) -> Result<(), PrimitiveError> {
if !self.removal {
return Err(PrimitiveError { operation: "BTRFS_IOC_SNAP_DESTROY", path: path.to_path_buf(), errno: libc::EPERM });
}
let metadata: std::fs::Metadata = std::fs::metadata(path).map_err(|error: std::io::Error| {
return PrimitiveError { operation: "BTRFS_IOC_SNAP_DESTROY", path: path.to_path_buf(), errno: error.raw_os_error().unwrap_or(0) };
})?;
std::fs::remove_dir_all(path).map_err(|error: std::io::Error| {
return PrimitiveError { operation: "BTRFS_IOC_SNAP_DESTROY", path: path.to_path_buf(), errno: error.raw_os_error().unwrap_or(0) };
})?;
self.subvolumes.lock().expect("the registry of subvolumes").remove(&(metadata.dev(), metadata.ino()));
return Ok(());
}
fn removal_allowed(&self, _path: &Path) -> bool {
return self.removal;
}
fn is_mount_point(&self, _path: &Path) -> bool {
return self.mount_point;
}
fn clone_file(&self, source: &File, destination: &File, path: &Path) -> Result<(), PrimitiveError> {
if self.real_clone {
return super::fs_clone_exchange::clone_file(source, destination, path);
}
return Err(PrimitiveError { operation: "FICLONE", path: path.to_path_buf(), errno: libc::EOPNOTSUPP });
}
fn exchange(&self, first: &Path, second: &Path) -> Result<(), PrimitiveError> {
return exchange_paths(first, second);
}
}
pub(crate) struct Hooked {
pub inner: PlainPlatform,
pub on_clone: Box<dyn Fn(u32)>,
pub calls: std::cell::Cell<u32>,
pub links_fail: bool,
}
impl Platform for Hooked {
fn filesystem_facts(&self, path: &Path) -> Result<(FilesystemKind, u64), PrimitiveError> {
return self.inner.filesystem_facts(path);
}
fn is_subvolume_root(&self, path: &Path) -> Result<bool, PrimitiveError> {
return self.inner.is_subvolume_root(path);
}
fn create_subvolume(&self, path: &Path) -> Result<(), PrimitiveError> {
return self.inner.create_subvolume(path);
}
fn destroy_subvolume(&self, path: &Path) -> Result<(), PrimitiveError> {
return self.inner.destroy_subvolume(path);
}
fn removal_allowed(&self, path: &Path) -> bool {
return self.inner.removal_allowed(path);
}
fn is_mount_point(&self, path: &Path) -> bool {
return self.inner.is_mount_point(path);
}
fn clone_file(&self, source: &File, destination: &File, path: &Path) -> Result<(), PrimitiveError> {
let calls: u32 = self.calls.get();
self.calls.set(calls + 1);
(self.on_clone)(calls);
return self.inner.clone_file(source, destination, path);
}
fn exchange(&self, first: &Path, second: &Path) -> Result<(), PrimitiveError> {
return self.inner.exchange(first, second);
}
fn hard_link(&self, existing: &Path, new: &Path) -> std::io::Result<()> {
if self.links_fail {
return Err(std::io::Error::from_raw_os_error(libc::EXDEV));
}
return std::fs::hard_link(existing, new);
}
}
impl Hooked {
pub(crate) fn new(hook: Box<dyn Fn(u32)>) -> Hooked {
return Hooked { inner: PlainPlatform::new(), on_clone: hook, calls: std::cell::Cell::new(0), links_fail: false };
}
}
pub(crate) fn backdate(root: &Path) {
let seconds: i64 = i64::try_from(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).expect("clock").as_secs()).expect("seconds") - 900;
let times: [libc::timespec; 2] = [libc::timespec { tv_sec: seconds, tv_nsec: 0 }, libc::timespec { tv_sec: seconds, tv_nsec: 0 }];
let mut pending: Vec<PathBuf> = vec![root.to_path_buf()];
while let Some(directory) = pending.pop() {
for item in std::fs::read_dir(&directory).expect("listing").flatten() {
if item.file_name() == ".git" {
continue;
}
let path: PathBuf = item.path();
if item.file_type().expect("type").is_dir() {
pending.push(path.clone());
}
let name = std::ffi::CString::new(path.as_os_str().as_encoded_bytes()).expect("path text");
let result: libc::c_int = unsafe { libc::utimensat(libc::AT_FDCWD, name.as_ptr(), times.as_ptr(), libc::AT_SYMLINK_NOFOLLOW) };
assert_eq!(result, 0, "backdate {}", path.display());
}
}
}
pub(crate) fn make_checkout(parent: &Path, name: &str) -> PathBuf {
let root: PathBuf = repository(parent, name);
std::fs::create_dir_all(root.join("src/deep/er")).expect("directories");
std::fs::write(root.join("README.md"), b"readme\n").expect("readme");
std::fs::write(root.join("src/main.rs"), b"fn main() {}\n").expect("main");
std::fs::write(root.join("src/deep/er/leaf.txt"), vec![b'x'; 70_000]).expect("leaf");
std::fs::write(root.join("empty"), b"").expect("empty");
std::fs::write(root.join("script.sh"), b"an executable file that is not run\n").expect("script");
std::fs::set_permissions(root.join("script.sh"), std::os::unix::fs::PermissionsExt::from_mode(0o755)).expect("executable");
std::os::unix::fs::symlink("src/main.rs", root.join("link")).expect("symlink");
std::fs::write(root.join(".gitignore"), b"ignored/\n").expect("gitignore");
std::fs::create_dir_all(root.join("ignored/node_modules/pkg")).expect("ignored tree");
std::fs::write(root.join("ignored/node_modules/pkg/index.js"), b"module.exports = 1;\n").expect("ignored file");
std::fs::hard_link(root.join("src/main.rs"), root.join("ignored/main-link.rs")).expect("hard link");
backdate(root.as_path());
git(root.as_path(), &["add", "--all"]);
git(root.as_path(), &["commit", "--quiet", "--message=files"]);
return root;
}
pub(crate) fn migrate_arguments() -> MigrateArguments {
return MigrateArguments {
mode: MigrateMode::Migrate,
dry_run: false,
nested_worktrees: true,
refresh_indexes: true,
discard_unreadable: false,
help: false,
};
}
pub(crate) fn run_command(platform: &dyn Platform, arguments: &MigrateArguments, root: &Path, interrupt: &Interrupt) -> (i32, Vec<String>) {
let mut lines: Vec<String> = Vec::new();
let code: i32 = execute(platform, arguments, root, interrupt, &mut |text: &str| { lines.push(text.to_string()); });
return (code, lines);
}
pub(crate) fn checkout_with_worktree(name: &str) -> (PathBuf, PathBuf) {
let parent: PathBuf = fixture(name);
let root: PathBuf = make_checkout(parent.as_path(), "checkout");
git(root.as_path(), &["worktree", "add", "--quiet", "--detach", "wt"]);
let exclude: PathBuf = root.join(".git/info/exclude");
let mut text: String = std::fs::read_to_string(exclude.as_path()).unwrap_or_default();
text.push_str("/wt/\n");
std::fs::create_dir_all(root.join(".git/info")).expect("info directory");
std::fs::write(exclude.as_path(), text).expect("exclude file");
return (parent, root);
}
pub(crate) fn index_inode(root: &Path, path: &str) -> u64 {
let text: String = git_text(root, &["ls-files", "--debug", "--", path]);
let line: &str = text.lines().find(|candidate: &&str| { return candidate.contains("ino:"); }).expect("a stat line");
return line.rsplit("ino:").next().expect("an inode").trim().parse::<u64>().expect("a number");
}