Skip to main content

fxcp_core/consistency/
journal.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2// Copyright (C) 2025 Joel Wirāmu Pauling <aenertia@aenertia.net>
3//
4// fxcp-core/src/consistency/journal.rs  --  Event journal for durable operation tracking
5
6//! Durable event journal for recording filesystem operations.
7
8use std::path::{Path};
9use std::fs::File;
10use std::os::unix::io::AsRawFd;
11use std::io;
12use tracing::{debug, warn};
13use libc;
14use std::ffi::CString;
15use std::os::unix::ffi::OsStrExt;
16use std::process::Command;
17
18const XFS_IOC_EXCHANGE_RANGE: u64 = 0xC0385828;
19const XFS_EXCHANGE_RANGE_TO_EOF: u64 = 1 << 0;
20#[repr(C)]
21struct xfs_exchange_range {
22    file1_fd: i32,
23    pad: i32,
24    file1_offset: u64,
25    file2_offset: u64,
26    length: u64,
27    flags: u64,
28    pad2: [u64; 2],
29}
30pub fn atomic_commit(temp_path: &Path, target_path: &Path) -> io::Result<()> {
31    if let (Ok(temp_file), Ok(target_file)) = (File::open(temp_path), File::open(target_path)) {
32        let temp_fd = temp_file.as_raw_fd();
33        let target_fd = target_file.as_raw_fd();
34        let args = xfs_exchange_range {
35            file1_fd: temp_fd,
36            pad: 0,
37            file1_offset: 0,
38            file2_offset: 0,
39            length: 0,
40            flags: XFS_EXCHANGE_RANGE_TO_EOF,
41            pad2: [0; 2],
42        };
43        // SAFETY: target_fd is a valid open fd. args is a repr(C) struct
44        // matching the kernel's xfs_exchange_range layout.
45        let ret = unsafe {
46            libc::ioctl(target_fd, XFS_IOC_EXCHANGE_RANGE, &args)
47        };
48        if ret == 0 {
49            debug!("Atomic Commit: XFS Exchange Range successful for {:?}", target_path);
50            let _ = std::fs::remove_file(temp_path);
51            return Ok(());
52        } else {
53            let err = io::Error::last_os_error();
54            match err.raw_os_error() {
55                Some(libc::EOPNOTSUPP) | Some(libc::ENOTTY) | Some(libc::EINVAL) => {
56                    debug!("Atomic Commit: IOCTL failed ({}), falling back to rename.", err);
57                },
58                _ => {
59                    warn!("Atomic Commit: Unexpected IOCTL error: {}. Falling back to rename.", err);
60                }
61            }
62        }
63    }
64    atomic_rename(temp_path, target_path, None)
65}
66pub fn atomic_rename(src: &Path, dst: &Path, flags: Option<u32>) -> io::Result<()> {
67    if let Some(parent) = dst.parent()
68        && !parent.exists() {
69            let _ = std::fs::create_dir_all(parent);
70        }
71    let flags = flags.unwrap_or(0);
72    
73    // Check if we need advanced syscalls
74    if flags & (libc::RENAME_EXCHANGE | libc::RENAME_NOREPLACE) != 0 {
75        let src_c = CString::new(src.as_os_str().as_bytes())?;
76        let dst_c = CString::new(dst.as_os_str().as_bytes())?;
77        // SAFETY: src_c and dst_c are valid null-terminated C strings.
78        // AT_FDCWD uses the process current directory. flags contains
79        // valid rename flags (RENAME_EXCHANGE or RENAME_NOREPLACE).
80        let ret = unsafe {
81            libc::renameat2(
82                libc::AT_FDCWD,
83                src_c.as_ptr(),
84                libc::AT_FDCWD,
85                dst_c.as_ptr(),
86                flags
87            )
88        };
89        
90        if ret == 0 {
91            return Ok(());
92        } else {
93            let err = io::Error::last_os_error();
94            let errno = err.raw_os_error().unwrap_or(0);
95
96            // Handle RENAME_EXCHANGE Failure
97            if (flags & libc::RENAME_EXCHANGE) != 0 {
98                // If supported failed or logical error, attempt fallback
99                debug!("Atomic Commit: RENAME_EXCHANGE failed ({}), attempting userspace fallback.", err);
100
101                // Fallback Logic
102                let temp_swap_path = dst.with_extension("exchange_bak");
103                
104                // Try Reflink Snapshot first (Safer)
105                let reflink_status = Command::new("cp")
106                    .arg("--reflink=always")
107                    .arg(dst)
108                    .arg(&temp_swap_path)
109                    .status();
110
111                let reflink_success = reflink_status.map(|s| s.success()).unwrap_or(false);
112
113                if reflink_success {
114                    // Step 3: Reflink succeeded
115                    // 1. Rename src -> dst (overwrite dst, we have backup)
116                    if let Err(e) = std::fs::rename(src, dst) {
117                        // Rollback attempt: remove temp
118                        let _ = std::fs::remove_file(&temp_swap_path);
119                        return Err(e);
120                    }
121                    // 2. Rename temp -> src (complete swap)
122                    if let Err(e) = std::fs::rename(&temp_swap_path, src) {
123                        warn!("Atomic Swap (Reflink): Failed to move temp back to src: {}. State inconsistent.", e);
124                        return Err(e);
125                    }
126                    return Ok(());
127                } else {
128                    // Step 4: Reflink failed, try Rename Dance
129                    warn!("Atomic Swap: Reflink fallback failed, attempting standard rename dance.");
130                    // 1. dst -> temp
131                    if let Err(e) = std::fs::rename(dst, &temp_swap_path) {
132                         if e.kind() == io::ErrorKind::NotFound {
133                             // Destination doesn't exist, proceed to simple rename src->dst at end of function
134                         } else {
135                             return Err(e); 
136                         }
137                    } else {
138                        // 2. src -> dst
139                        if let Err(e) = std::fs::rename(src, dst) {
140                            // Rollback: temp -> dst
141                            if let Err(rollback_err) = std::fs::rename(&temp_swap_path, dst) {
142                                tracing::error!(
143                                    path = %dst.display(),
144                                    temp = %temp_swap_path.display(),
145                                    err = %rollback_err,
146                                    "CRITICAL: journal atomic swap failed AND rollback failed — data may be lost"
147                                );
148                            }
149                            return Err(e);
150                        }
151                        // 3. temp -> src
152                        if let Err(e) = std::fs::rename(&temp_swap_path, src) {
153                            warn!("Atomic Swap (Dance): Failed to move temp to src: {}.", e);
154                            return Err(e);
155                        }
156                        return Ok(());
157                    }
158                }
159            } else {
160                // Handle RENAME_NOREPLACE Failure
161                if (flags & libc::RENAME_NOREPLACE) != 0 {
162                    // If target exists, NOREPLACE *should* fail. Do not fallback to overwrite.
163                    if errno == libc::EEXIST {
164                        return Err(err);
165                    }
166                }
167                
168                // If failure is due to lack of support, fall through to std::fs::rename.
169                if errno == libc::EINVAL || errno == libc::EOPNOTSUPP || errno == libc::ENOSYS {
170                    debug!("atomic_rename: Advanced flags ({}) not supported ({}). Falling back to standard rename.", flags, errno);
171                    // Fall through to std::fs::rename below
172                } else {
173                    // Actual IO error (e.g. permission denied), return it.
174                    return Err(err);
175                }
176            }
177        }
178    }
179    
180    // Standard POSIX rename (overwrites dst if it exists)
181    std::fs::rename(src, dst)
182}