Skip to main content

uu_sync/
sync.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5
6/* Last synced with: sync (GNU coreutils) 8.13 */
7
8use clap::{Arg, ArgAction, Command};
9#[cfg(any(target_os = "linux", target_os = "android"))]
10use nix::errno::Errno;
11#[cfg(any(target_os = "linux", target_os = "android"))]
12use nix::fcntl::{OFlag, open};
13#[cfg(any(target_os = "linux", target_os = "android"))]
14use nix::sys::stat::Mode;
15use std::path::Path;
16use uucore::display::Quotable;
17use uucore::error::{UResult, USimpleError, get_exit_code, set_exit_code};
18use uucore::format_usage;
19use uucore::show_error;
20use uucore::translate;
21
22pub mod options {
23    pub static FILE_SYSTEM: &str = "file-system";
24    pub static DATA: &str = "data";
25}
26
27static ARG_FILES: &str = "files";
28
29#[cfg(unix)]
30mod platform {
31    #[cfg(any(target_os = "linux", target_os = "android"))]
32    use nix::fcntl::{FcntlArg, OFlag, fcntl};
33    use nix::unistd::sync;
34    #[cfg(any(target_os = "linux", target_os = "android"))]
35    use nix::unistd::{fdatasync, syncfs};
36    #[cfg(any(target_os = "linux", target_os = "android"))]
37    use std::fs::{File, OpenOptions};
38    #[cfg(any(target_os = "linux", target_os = "android"))]
39    use std::os::unix::fs::OpenOptionsExt;
40    #[cfg(any(target_os = "linux", target_os = "android"))]
41    use uucore::display::Quotable;
42    #[cfg(any(target_os = "linux", target_os = "android"))]
43    use uucore::error::FromIo;
44    #[cfg(any(target_os = "linux", target_os = "android"))]
45    use uucore::translate;
46
47    use uucore::error::UResult;
48
49    #[expect(
50        clippy::unnecessary_wraps,
51        reason = "fn sig must match on all platforms"
52    )]
53    pub fn do_sync() -> UResult<()> {
54        sync();
55        Ok(())
56    }
57
58    /// Opens a file and resets its O_NONBLOCK flag to match GNU behavior.
59    /// Returns the opened file or an error if opening fails.
60    /// Logs a warning if fcntl fails but doesn't abort the operation.
61    #[cfg(any(target_os = "linux", target_os = "android"))]
62    fn open_and_reset_nonblock(path: &str) -> UResult<File> {
63        let f = OpenOptions::new()
64            .read(true)
65            .custom_flags(OFlag::O_NONBLOCK.bits())
66            .open(path)
67            .map_err_context(|| path.to_string())?;
68        // Reset O_NONBLOCK flag if it was set (matches GNU behavior)
69        // This is non-critical, so we log errors but don't fail
70        if let Err(e) = fcntl(&f, FcntlArg::F_SETFL(OFlag::empty())) {
71            eprintln!(
72                "sync: {}",
73                translate!("sync-warning-fcntl-failed", "file" => path, "error" => e.to_string())
74            );
75        }
76        Ok(f)
77    }
78
79    #[cfg(any(target_os = "linux", target_os = "android"))]
80    pub fn do_syncfs(files: Vec<String>) -> UResult<()> {
81        for path in files {
82            let f = open_and_reset_nonblock(&path)?;
83            syncfs(f).map_err_context(
84                || translate!("sync-error-syncing-file", "file" => path.quote()),
85            )?;
86        }
87        Ok(())
88    }
89
90    #[cfg(any(target_os = "linux", target_os = "android"))]
91    pub fn do_fdatasync(files: Vec<String>) -> UResult<()> {
92        for path in files {
93            let f = open_and_reset_nonblock(&path)?;
94            fdatasync(f).map_err_context(
95                || translate!("sync-error-syncing-file", "file" => path.quote()),
96            )?;
97        }
98        Ok(())
99    }
100}
101
102#[cfg(windows)]
103mod platform {
104    use std::fs::OpenOptions;
105    use std::os::windows::prelude::*;
106    use std::path::Path;
107    use uucore::error::{UResult, USimpleError};
108    use uucore::translate;
109    use uucore::wide::{FromWide, ToWide};
110    use windows_sys::Win32::Foundation::{
111        ERROR_NO_MORE_FILES, GetLastError, HANDLE, INVALID_HANDLE_VALUE, MAX_PATH,
112    };
113    use windows_sys::Win32::Storage::FileSystem::{
114        FindFirstVolumeW, FindNextVolumeW, FindVolumeClose, FlushFileBuffers, GetDriveTypeW,
115    };
116    use windows_sys::Win32::System::WindowsProgramming::DRIVE_FIXED;
117
118    fn get_last_error() -> u32 {
119        // SAFETY: `GetLastError` has no safety preconditions
120        unsafe { GetLastError() as u32 }
121    }
122
123    fn flush_volume(name: &str) -> UResult<()> {
124        let name_wide = name.to_wide_null();
125        // SAFETY: `name` is a valid `str`, so `name_wide` is valid null-terminated UTF-16
126        if unsafe { GetDriveTypeW(name_wide.as_ptr()) } == DRIVE_FIXED {
127            let sliced_name = &name[..name.len() - 1]; // eliminate trailing backslash
128            match OpenOptions::new().write(true).open(sliced_name) {
129                Ok(file) => {
130                    // SAFETY: `file` is a valid `File`
131                    if unsafe { FlushFileBuffers(file.as_raw_handle() as HANDLE) } == 0 {
132                        Err(USimpleError::new(
133                            get_last_error() as i32,
134                            translate!("sync-error-flush-file-buffer"),
135                        ))
136                    } else {
137                        Ok(())
138                    }
139                }
140                Err(e) => Err(USimpleError::new(
141                    e.raw_os_error().unwrap_or(1),
142                    translate!("sync-error-create-volume-handle"),
143                )),
144            }
145        } else {
146            Ok(())
147        }
148    }
149
150    fn find_first_volume() -> UResult<(String, HANDLE)> {
151        let mut name: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize];
152        // SAFETY: `name` was just constructed and in scope, `len()` is its length by definition
153        let handle = unsafe { FindFirstVolumeW(name.as_mut_ptr(), name.len() as u32) };
154        if handle == INVALID_HANDLE_VALUE {
155            return Err(USimpleError::new(
156                get_last_error() as i32,
157                translate!("sync-error-find-first-volume"),
158            ));
159        }
160        Ok((String::from_wide_null(&name), handle))
161    }
162
163    fn find_all_volumes() -> UResult<Vec<String>> {
164        let (first_volume, next_volume_handle) = find_first_volume()?;
165        let mut volumes = vec![first_volume];
166        loop {
167            let mut name: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize];
168            // SAFETY: `next_volume_handle` was returned by `find_first_volume`,
169            // `name` was just constructed and in scope, `len()` is its length by definition
170            if unsafe { FindNextVolumeW(next_volume_handle, name.as_mut_ptr(), name.len() as u32) }
171                == 0
172            {
173                return match get_last_error() {
174                    ERROR_NO_MORE_FILES => {
175                        // SAFETY: `next_volume_handle` was returned by `find_first_volume`
176                        unsafe { FindVolumeClose(next_volume_handle) };
177                        Ok(volumes)
178                    }
179                    err => Err(USimpleError::new(
180                        err as i32,
181                        translate!("sync-error-find-next-volume"),
182                    )),
183                };
184            }
185            volumes.push(String::from_wide_null(&name));
186        }
187    }
188
189    pub fn do_sync() -> UResult<()> {
190        let volumes = find_all_volumes()?;
191        for vol in &volumes {
192            flush_volume(vol)?;
193        }
194        Ok(())
195    }
196
197    pub fn do_syncfs(files: Vec<String>) -> UResult<()> {
198        for path in files {
199            let maybe_first = Path::new(&path).components().next();
200            let vol_name = match maybe_first {
201                Some(c) => c.as_os_str().to_string_lossy().into_owned(),
202                None => {
203                    return Err(USimpleError::new(
204                        1,
205                        translate!("sync-error-no-such-file", "file" => path),
206                    ));
207                }
208            };
209            flush_volume(&vol_name)?;
210        }
211        Ok(())
212    }
213}
214
215#[uucore::main]
216pub fn uumain(args: impl uucore::Args) -> UResult<()> {
217    let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
218    let files: Vec<String> = matches
219        .get_many::<String>(ARG_FILES)
220        .map(|v| v.map(ToString::to_string).collect())
221        .unwrap_or_default();
222
223    if matches.get_flag(options::DATA) && files.is_empty() {
224        return Err(USimpleError::new(
225            1,
226            translate!("sync-error-data-needs-argument"),
227        ));
228    }
229
230    for f in &files {
231        // Use the Nix open to be able to set the NONBLOCK flags for fifo files
232        #[cfg(any(target_os = "linux", target_os = "android"))]
233        {
234            let path = Path::new(&f);
235            if let Err(e) = open(path, OFlag::O_NONBLOCK, Mode::empty()) {
236                if e != Errno::EACCES || (e == Errno::EACCES && path.is_dir()) {
237                    show_error!(
238                        "{}",
239                        translate!("sync-error-opening-file", "file" => f.quote(), "err" => e.desc())
240                    );
241                    set_exit_code(1);
242                }
243            }
244        }
245        #[cfg(not(any(target_os = "linux", target_os = "android")))]
246        {
247            if !Path::new(&f).exists() {
248                show_error!(
249                    "{}",
250                    translate!("sync-error-no-such-file", "file" => f.quote())
251                );
252                set_exit_code(1);
253            }
254        }
255    }
256
257    if get_exit_code() != 0 {
258        return Err(USimpleError::new(1, ""));
259    }
260
261    #[allow(clippy::if_same_then_else)]
262    if matches.get_flag(options::FILE_SYSTEM) {
263        #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
264        syncfs(files)?;
265    } else if matches.get_flag(options::DATA) {
266        #[cfg(any(target_os = "linux", target_os = "android"))]
267        fdatasync(files)?;
268    } else {
269        sync()?;
270    }
271    Ok(())
272}
273
274pub fn uu_app() -> Command {
275    Command::new(uucore::util_name())
276        .version(uucore::crate_version!())
277        .help_template(uucore::localized_help_template(uucore::util_name()))
278        .about(translate!("sync-about"))
279        .override_usage(format_usage(&translate!("sync-usage")))
280        .infer_long_args(true)
281        .arg(
282            Arg::new(options::FILE_SYSTEM)
283                .short('f')
284                .long(options::FILE_SYSTEM)
285                .conflicts_with(options::DATA)
286                .help(translate!("sync-help-file-system"))
287                .action(ArgAction::SetTrue),
288        )
289        .arg(
290            Arg::new(options::DATA)
291                .short('d')
292                .long(options::DATA)
293                .conflicts_with(options::FILE_SYSTEM)
294                .help(translate!("sync-help-data"))
295                .action(ArgAction::SetTrue),
296        )
297        .arg(
298            Arg::new(ARG_FILES)
299                .action(ArgAction::Append)
300                .value_hint(clap::ValueHint::AnyPath),
301        )
302}
303
304fn sync() -> UResult<()> {
305    platform::do_sync()
306}
307
308#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
309fn syncfs(files: Vec<String>) -> UResult<()> {
310    platform::do_syncfs(files)
311}
312
313#[cfg(any(target_os = "linux", target_os = "android"))]
314fn fdatasync(files: Vec<String>) -> UResult<()> {
315    platform::do_fdatasync(files)
316}