diff --git a/Cargo.lock b/Cargo.lock index 805c1c83a..81c35d28b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3171,6 +3171,7 @@ dependencies = [ "filetime", "time", "uucore", + "winapi 0.3.9", ] [[package]] diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index 606a7ecc4..bb6e0dc1e 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -20,6 +20,9 @@ clap = { version = "3.0", features = ["wrap_help", "cargo"] } time = "0.1.40" uucore = { version=">=0.0.11", package="uucore", path="../../uucore", features=["libc"] } +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3" } + [[bin]] name = "touch" path = "src/main.rs" diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 620538bbf..2b56cdb95 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -6,7 +6,7 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -// spell-checker:ignore (ToDO) filetime strptime utcoff strs datetime MMDDhhmm clapv +// spell-checker:ignore (ToDO) filetime strptime utcoff strs datetime MMDDhhmm clapv PWSTR lpszfilepath hresult pub extern crate filetime; @@ -16,7 +16,7 @@ extern crate uucore; use clap::{crate_version, App, AppSettings, Arg, ArgGroup}; use filetime::*; use std::fs::{self, File}; -use std::path::Path; +use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::format_usage; @@ -77,7 +77,15 @@ Try 'touch --help' for more information."##, }; for filename in files { - let path = Path::new(filename); + // FIXME: find a way to avoid having to clone the path + let pathbuf = if filename == "-" { + pathbuf_from_stdout()? + } else { + PathBuf::from(filename) + }; + + let path = pathbuf.as_path(); + if !path.exists() { if matches.is_present(options::NO_CREATE) { continue; @@ -299,3 +307,89 @@ fn parse_timestamp(s: &str) -> UResult { Ok(ft) } + +// TODO: this may be a good candidate to put in fsext.rs +/// Returns a PathBuf to stdout. +/// +/// On Windows, uses GetFinalPathNameByHandleW to attempt to get the path +/// from the stdout handle. +fn pathbuf_from_stdout() -> UResult { + #[cfg(unix)] + { + Ok(PathBuf::from("/dev/stdout")) + } + #[cfg(windows)] + { + use std::os::windows::prelude::AsRawHandle; + use winapi::shared::minwindef::{DWORD, MAX_PATH}; + use winapi::shared::winerror::{ + ERROR_INVALID_PARAMETER, ERROR_NOT_ENOUGH_MEMORY, ERROR_PATH_NOT_FOUND, + }; + use winapi::um::errhandlingapi::GetLastError; + use winapi::um::fileapi::GetFinalPathNameByHandleW; + use winapi::um::winnt::WCHAR; + + let handle = std::io::stdout().lock().as_raw_handle(); + let mut file_path_buffer: [WCHAR; MAX_PATH as usize] = [0; MAX_PATH as usize]; + + // Couldn't find this in winapi + const FILE_NAME_OPENED: DWORD = 0x8; + + // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlea#examples + // SAFETY: We transmute the handle to be able to cast *mut c_void into a + // HANDLE (i32) so rustc will let us call GetFinalPathNameByHandleW. The + // reference example code for GetFinalPathNameByHandleW implies that + // it is safe for us to leave lpszfilepath uninitialized, so long as + // the buffer size is correct. We know the buffer size (MAX_PATH) at + // compile time. MAX_PATH is a small number (260) so we can cast it + // to a u32. + let ret = unsafe { + GetFinalPathNameByHandleW( + std::mem::transmute(handle), + file_path_buffer.as_mut_ptr(), + file_path_buffer.len() as u32, + FILE_NAME_OPENED, + ) + }; + + let buffer_size = match ret { + ERROR_PATH_NOT_FOUND | ERROR_NOT_ENOUGH_MEMORY | ERROR_INVALID_PARAMETER => { + return Err(USimpleError::new( + 1, + format!("GetFinalPathNameByHandleW failed with code {}", ret), + )) + } + e if e == 0 => { + return Err(USimpleError::new( + 1, + format!( + "GetFinalPathNameByHandleW failed with code {}", + // SAFETY: GetLastError is thread-safe and has no documented memory unsafety. + unsafe { GetLastError() } + ), + )); + } + e => e as usize, + }; + + // Don't include the null terminator + Ok(String::from_utf16(&file_path_buffer[0..buffer_size]) + .map_err(|e| USimpleError::new(1, e.to_string()))? + .into()) + } +} + +#[cfg(test)] +mod tests { + #[cfg(windows)] + #[test] + fn test_get_pathbuf_from_stdout_fails_if_stdout_is_not_a_file() { + // We can trigger an error by not setting stdout to anything (will + // fail with code 1) + assert!(super::pathbuf_from_stdout() + .err() + .expect("pathbuf_from_stdout should have failed") + .to_string() + .contains("GetFinalPathNameByHandleW failed with code 1")); + } +} diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index ac82a81ea..f869d1560 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -510,6 +510,27 @@ fn test_touch_no_such_file_error_msg() { )); } +#[test] +fn test_touch_changes_time_of_file_in_stdout() { + // command like: `touch - 1< ./c` + // should change the timestamp of c + + let (at, mut ucmd) = at_and_ucmd!(); + let file = "test_touch_changes_time_of_file_in_stdout"; + + at.touch(file); + assert!(at.file_exists(file)); + let (_, mtime) = get_file_times(&at, file); + + ucmd.args(&["-"]) + .set_stdout(at.make_file(file)) + .succeeds() + .no_stderr(); + + let (_, mtime_after) = get_file_times(&at, file); + assert!(mtime_after != mtime); +} + #[test] #[cfg(unix)] fn test_touch_permission_denied_error_msg() {