diff --git a/Cargo.lock b/Cargo.lock index 9d9fb6c24..2ba6cf6a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1475,8 +1475,10 @@ version = "0.0.4" dependencies = [ "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.85 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 4e3227f02..c62cfe2b3 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -20,6 +20,12 @@ clap = "2.33" uucore = { version=">=0.0.7", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[target.'cfg(windows)'.dependencies] +winapi = { version = "0.3", features = ["minwinbase", "sysinfoapi", "minwindef"] } + [[bin]] name = "date" path = "src/main.rs" diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 5ee4b1610..43573437d 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -12,13 +12,20 @@ #[macro_use] extern crate uucore; +use chrono::{DateTime, FixedOffset, Local, Offset, Utc}; +#[cfg(windows)] +use chrono::{Datelike, Timelike}; use clap::{App, Arg}; - -use chrono::offset::Utc; -use chrono::{DateTime, FixedOffset, Local, Offset}; +#[cfg(all(unix, not(target_os = "macos")))] +use libc::{clock_settime, timespec, CLOCK_REALTIME}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; +#[cfg(windows)] +use winapi::{ + shared::minwindef::WORD, + um::{minwinbase::SYSTEMTIME, sysinfoapi::SetSystemTime}, +}; // Options const DATE: &str = "date"; @@ -62,6 +69,11 @@ static RFC_3339_HELP_STRING: &str = "output date/time in RFC 3339 format. for date and time to the indicated precision. Example: 2006-08-14 02:34:56-06:00"; +#[cfg(not(target_os = "macos"))] +static OPT_SET_HELP_STRING: &str = "set time described by STRING"; +#[cfg(target_os = "macos")] +static OPT_SET_HELP_STRING: &str = "set time described by STRING (not available on mac yet)"; + /// Settings for this program, parsed from the command line struct Settings { utc: bool, @@ -186,7 +198,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .short("s") .long(OPT_SET) .takes_value(true) - .help("set time described by STRING"), + .help(OPT_SET_HELP_STRING), ) .arg( Arg::with_name(OPT_UNIVERSAL) @@ -222,18 +234,31 @@ pub fn uumain(args: impl uucore::Args) -> i32 { DateSource::Now }; + let set_to = match matches.value_of(OPT_SET).map(parse_date) { + None => None, + Some(Err((input, _err))) => { + eprintln!("date: invalid date '{}'", input); + return 1; + } + Some(Ok(date)) => Some(date), + }; + let settings = Settings { utc: matches.is_present(OPT_UNIVERSAL), format, date_source, - // TODO: Handle this option: - set_to: None, + set_to, }; - if let Some(_time) = settings.set_to { - unimplemented!(); - // Probably need to use this syscall: - // https://doc.rust-lang.org/libc/i686-unknown-linux-gnu/libc/fn.clock_settime.html + if let Some(date) = settings.set_to { + // All set time functions expect UTC datetimes. + let date: DateTime = if settings.utc { + date.with_timezone(&Utc) + } else { + date.into() + }; + + return set_system_datetime(date); } else { // Declare a file here because it needs to outlive the `dates` iterator. let file: File; @@ -247,15 +272,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { now.with_timezone(now.offset()) }; - /// Parse a `String` into a `DateTime`. - /// If it fails, return a tuple of the `String` along with its `ParseError`. - fn parse_date( - s: String, - ) -> Result, (String, chrono::format::ParseError)> { - // TODO: The GNU date command can parse a wide variety of inputs. - s.parse().map_err(|e| (s, e)) - } - // Iterate over all dates - whether it's a single date or a file. let dates: Box> = match settings.date_source { DateSource::Custom(ref input) => { @@ -314,3 +330,76 @@ fn make_format_string(settings: &Settings) -> &str { Format::Default => "%c", } } + +/// Parse a `String` into a `DateTime`. +/// If it fails, return a tuple of the `String` along with its `ParseError`. +fn parse_date + Clone>( + s: S, +) -> Result, (String, chrono::format::ParseError)> { + // TODO: The GNU date command can parse a wide variety of inputs. + s.as_ref().parse().map_err(|e| (s.as_ref().into(), e)) +} + +#[cfg(not(any(unix, windows)))] +fn set_system_datetime(_date: DateTime) -> i32 { + unimplemented!("setting date not implemented (unsupported target)"); +} + +#[cfg(target_os = "macos")] +fn set_system_datetime(_date: DateTime) -> i32 { + eprintln!("date: setting the date is not supported by macOS"); + return 1; +} + +#[cfg(all(unix, not(target_os = "macos")))] +/// System call to set date (unix). +/// See here for more: +/// https://doc.rust-lang.org/libc/i686-unknown-linux-gnu/libc/fn.clock_settime.html +/// https://linux.die.net/man/3/clock_settime +/// https://www.gnu.org/software/libc/manual/html_node/Time-Types.html +fn set_system_datetime(date: DateTime) -> i32 { + let timespec = timespec { + tv_sec: date.timestamp() as _, + tv_nsec: date.timestamp_subsec_nanos() as _, + }; + + let result = unsafe { clock_settime(CLOCK_REALTIME, ×pec) }; + + if result != 0 { + let error = std::io::Error::last_os_error(); + eprintln!("date: cannot set date: {}", error); + error.raw_os_error().unwrap() + } else { + 0 + } +} + +#[cfg(windows)] +/// System call to set date (Windows). +/// See here for more: +/// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-setsystemtime +/// https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime +fn set_system_datetime(date: DateTime) -> i32 { + let system_time = SYSTEMTIME { + wYear: date.year() as WORD, + wMonth: date.month() as WORD, + // Ignored + wDayOfWeek: 0, + wDay: date.day() as WORD, + wHour: date.hour() as WORD, + wMinute: date.minute() as WORD, + wSecond: date.second() as WORD, + // TODO: be careful of leap seconds - valid range is [0, 999] - how to handle? + wMilliseconds: ((date.nanosecond() / 1_000_000) % 1000) as WORD, + }; + + let result = unsafe { SetSystemTime(&system_time) }; + + if result == 0 { + let error = std::io::Error::last_os_error(); + eprintln!("date: cannot set date: {}", error); + error.raw_os_error().unwrap() + } else { + 0 + } +} diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 0837878b2..652edfa25 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2,6 +2,8 @@ extern crate regex; use self::regex::Regex; use crate::common::util::*; +#[cfg(all(unix, not(target_os = "macos")))] +use rust_users::*; #[test] fn test_date_email() { @@ -131,3 +133,42 @@ fn test_date_format_full_day() { let re = Regex::new(r"\S+ \d{4}-\d{2}-\d{2}").unwrap(); assert!(re.is_match(&result.stdout.trim())); } + +#[test] +#[cfg(all(unix, not(target_os = "macos")))] +fn test_date_set_valid() { + if get_effective_uid() == 0 { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg("--set").arg("2020-03-12 13:30:00+08:00").succeeds(); + result.no_stdout().no_stderr(); + } +} + +#[test] +#[cfg(any(windows, all(unix, not(target_os = "macos"))))] +fn test_date_set_invalid() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg("--set").arg("123abcd").fails(); + let result = result.no_stdout(); + assert!(result.stderr.starts_with("date: invalid date ")); +} + +#[test] +#[cfg(all(unix, not(target_os = "macos")))] +fn test_date_set_permissions_error() { + if !(get_effective_uid() == 0 || is_wsl()) { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg("--set").arg("2020-03-11 21:45:00+08:00").fails(); + let result = result.no_stdout(); + assert!(result.stderr.starts_with("date: cannot set date: ")); + } +} + +#[test] +#[cfg(target_os = "macos")] +fn test_date_set_mac_unavailable() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg("--set").arg("2020-03-11 21:45:00+08:00").fails(); + let result = result.no_stdout(); + assert!(result.stderr.starts_with("date: setting the date is not supported by macOS")); +}