touch: implement - (#3158)

This commit is contained in:
Devin Gunay 2022-03-06 14:00:42 -08:00 committed by GitHub
parent 90cc2bff6a
commit 103dffc12e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 122 additions and 3 deletions

1
Cargo.lock generated
View file

@ -3171,6 +3171,7 @@ dependencies = [
"filetime",
"time",
"uucore",
"winapi 0.3.9",
]
[[package]]

View file

@ -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"

View file

@ -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<FileTime> {
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<PathBuf> {
#[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"));
}
}

View file

@ -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() {