Serenity Operating System
at master 253 lines 9.8 kB view raw
1/* 2 * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2022, Ariel Don <ariel@arieldon.com> 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include <AK/CheckedFormatString.h> 9#include <AK/GenericLexer.h> 10#include <AK/Time.h> 11#include <LibCore/ArgsParser.h> 12#include <LibCore/DeprecatedFile.h> 13#include <LibCore/System.h> 14#include <LibMain/Main.h> 15#include <LibTimeZone/TimeZone.h> 16#include <ctype.h> 17#include <errno.h> 18#include <fcntl.h> 19#include <stdio.h> 20#include <stdlib.h> 21#include <sys/stat.h> 22#include <time.h> 23 24static DeprecatedString program_name; 25 26template<typename... Parameters> 27[[noreturn]] static void err(CheckedFormatString<Parameters...>&& fmtstr, Parameters const&... parameters) 28{ 29 warn("{}: ", program_name); 30 warnln(move(fmtstr), parameters...); 31 exit(1); 32} 33 34inline bool validate_timestamp(unsigned year, unsigned month, unsigned day, unsigned hour, unsigned minute, unsigned second) 35{ 36 return (year >= 1970) && (month >= 1 && month <= 12) && (day >= 1 && day <= static_cast<unsigned>(days_in_month(year, month))) && (hour <= 23) && (minute <= 59) && (second <= 59); 37} 38 39static void parse_time(StringView input_time, timespec& atime, timespec& mtime) 40{ 41 // Parse [[CC]YY]MMDDhhmm[.SS] format, where brackets signify optional 42 // parameters. 43 if (input_time.length() < 8) 44 err("invalid time format '{}' -- too short", input_time); 45 else if (input_time.length() > 15) 46 err("invalid time format '{}' -- too long", input_time); 47 48 Vector<u8> parameters; 49 GenericLexer lexer(input_time); 50 unsigned year, month, day, hour, minute, second; 51 52 auto lex_number = [&]() { 53 auto literal = lexer.consume(2); 54 if (literal.length() < 2) 55 err("invalid time format '{}' -- expected 2 digits per parameter", input_time); 56 auto maybe_parameter = literal.to_uint(); 57 if (maybe_parameter.has_value()) 58 parameters.append(maybe_parameter.value()); 59 else 60 err("invalid time format '{}'", input_time); 61 }; 62 63 while (!lexer.is_eof() && lexer.next_is(isdigit)) 64 lex_number(); 65 if (parameters.size() > 6) 66 err("invalid time format '{}' -- too many parameters", input_time); 67 68 if (lexer.consume_specific('.')) { 69 lex_number(); 70 second = parameters.take_last(); 71 } else { 72 second = 0; 73 } 74 75 auto current_year = seconds_since_epoch_to_year(time(nullptr)); 76 auto current_century = current_year / 100; 77 if (parameters.size() == 6) 78 year = parameters.take_first() * 100 + parameters.take_first(); 79 else if (parameters.size() == 5) 80 year = current_century * 100 + parameters.take_first(); 81 else 82 year = current_year; 83 84 minute = parameters.take_last(); 85 hour = parameters.take_last(); 86 day = parameters.take_last(); 87 month = parameters.take_last(); 88 89 if (validate_timestamp(year, month, day, hour, minute, second)) 90 atime = mtime = AK::Time::from_timestamp(year, month, day, hour, minute, second, 0).to_timespec(); 91 else 92 err("invalid time format '{}'", input_time); 93} 94 95static void parse_datetime(StringView input_datetime, timespec& atime, timespec& mtime) 96{ 97 // Parse YYYY-MM-DDThh:mm:SS[.frac][tz] or YYYY-MM-DDThh:mm:SS[,frac][tz] 98 // formats, where brackets signify optional parameters. 99 GenericLexer lexer(input_datetime); 100 unsigned year, month, day, hour, minute, second, millisecond; 101 StringView time_zone; 102 103 auto lex_number = [&](unsigned& value, size_t n) { 104 auto maybe_value = lexer.consume(n).to_uint(); 105 if (!maybe_value.has_value()) 106 err("invalid datetime format '{}' -- expected number at index {}", input_datetime, lexer.tell()); 107 else 108 value = maybe_value.value(); 109 }; 110 111 lex_number(year, 4); 112 if (!lexer.consume_specific('-')) 113 err("invalid datetime format '{}' -- expected '-' after year", input_datetime); 114 lex_number(month, 2); 115 if (!lexer.consume_specific('-')) 116 err("invalid datetime format '{}' -- expected '-' after month", input_datetime); 117 lex_number(day, 2); 118 119 // Parse the time designator -- a single 'T' or ' ' according to POSIX. 120 if (!lexer.consume_specific('T') && !lexer.consume_specific(' ')) 121 err("invalid datetime format '{}' -- expected 'T' or ' ' for time designator", input_datetime); 122 123 lex_number(hour, 2); 124 if (!lexer.consume_specific(':')) 125 err("invalid datetime format '{}' -- expected ':' after hour", input_datetime); 126 lex_number(minute, 2); 127 if (!lexer.consume_specific(':')) 128 err("invalid datetime format '{}' -- expected ':' after minute", input_datetime); 129 lex_number(second, 2); 130 131 millisecond = 0; 132 if (!lexer.is_eof()) { 133 if (lexer.consume_specific(',') || lexer.consume_specific('.')) { 134 auto fractional_second = lexer.consume_while(isdigit); 135 if (fractional_second.is_empty()) 136 err("invalid datetime format '{}' -- expected floating seconds", input_datetime); 137 for (u8 i = 0; i < 3 && i < fractional_second.length(); ++i) { 138 unsigned n = fractional_second[i] - '0'; 139 switch (i) { 140 case 0: 141 millisecond += 100 * n; 142 break; 143 case 1: 144 millisecond += 10 * n; 145 break; 146 case 2: 147 millisecond += n; 148 break; 149 default: 150 VERIFY_NOT_REACHED(); 151 } 152 } 153 } 154 155 time_zone = lexer.consume_all(); 156 if (!time_zone.is_empty() && time_zone != "Z") 157 err("invalid datetime format '{}' -- failed to parse time zone", input_datetime); 158 } 159 160 if (validate_timestamp(year, month, day, hour, minute, second)) { 161 auto timestamp = AK::Time::from_timestamp(year, month, day, hour, minute, second, millisecond); 162 auto time = timestamp.to_timespec(); 163 if (time_zone.is_empty() && TimeZone::system_time_zone() != "UTC") { 164 auto offset = TimeZone::get_time_zone_offset(TimeZone::system_time_zone(), timestamp); 165 if (offset.has_value()) 166 time.tv_sec -= offset.value().seconds; 167 else 168 err("failed to get the system time zone"); 169 } 170 atime = mtime = time; 171 } else { 172 err("invalid datetime format '{}'", input_datetime); 173 } 174} 175 176static void reference_time(StringView reference_path, timespec& atime, timespec& mtime) 177{ 178 auto maybe_buffer = Core::System::stat(reference_path); 179 if (maybe_buffer.is_error()) 180 err("failed to reference times of '{}': {}", reference_path, maybe_buffer.release_error()); 181 auto buffer = maybe_buffer.release_value(); 182 atime.tv_sec = buffer.st_atime; 183 atime.tv_nsec = buffer.st_atim.tv_nsec; 184 mtime.tv_sec = buffer.st_mtime; 185 mtime.tv_nsec = buffer.st_mtim.tv_nsec; 186} 187 188ErrorOr<int> serenity_main(Main::Arguments arguments) 189{ 190 TRY(Core::System::pledge("stdio rpath cpath fattr")); 191 192 program_name = arguments.strings[0]; 193 194 Vector<DeprecatedString> paths; 195 196 timespec times[2]; 197 auto& atime = times[0]; 198 auto& mtime = times[1]; 199 200 bool update_atime = false; 201 bool update_mtime = false; 202 bool no_create_file = false; 203 204 DeprecatedString input_datetime = ""; 205 DeprecatedString input_time = ""; 206 DeprecatedString reference_path = ""; 207 208 Core::ArgsParser args_parser; 209 args_parser.set_general_help("Create a file or update file access time and/or modification time."); 210 args_parser.add_ignored(nullptr, 'f'); 211 args_parser.add_option(update_atime, "Change access time of file", nullptr, 'a'); 212 args_parser.add_option(no_create_file, "Do not create a file if it does not exist", nullptr, 'c'); 213 args_parser.add_option(update_mtime, "Change modification time of file", nullptr, 'm'); 214 args_parser.add_option(input_datetime, "Use specified datetime instead of current time", nullptr, 'd', "datetime"); 215 args_parser.add_option(input_time, "Use specified time instead of current time", nullptr, 't', "time"); 216 args_parser.add_option(reference_path, "Use time of file specified by reference path instead of current time", nullptr, 'r', "reference"); 217 args_parser.add_positional_argument(paths, "Files to touch", "path", Core::ArgsParser::Required::Yes); 218 args_parser.parse(arguments); 219 220 if (input_datetime.is_empty() + input_time.is_empty() + reference_path.is_empty() < 2) 221 err("cannot specify a time with more than one option"); 222 223 if (!input_datetime.is_empty()) 224 parse_datetime(input_datetime, atime, mtime); 225 else if (!input_time.is_empty()) 226 parse_time(input_time, atime, mtime); 227 else if (!reference_path.is_empty()) 228 reference_time(reference_path, atime, mtime); 229 else 230 atime.tv_nsec = mtime.tv_nsec = UTIME_NOW; 231 232 // According to POSIX, if neither -a nor -m are specified, then the program 233 // should behave as if both are specified. 234 if (!update_atime && !update_mtime) 235 update_atime = update_mtime = true; 236 if (update_atime && !update_mtime) 237 mtime.tv_nsec = UTIME_OMIT; 238 if (update_mtime && !update_atime) 239 atime.tv_nsec = UTIME_OMIT; 240 241 for (auto path : paths) { 242 if (Core::DeprecatedFile::exists(path)) { 243 if (utimensat(AT_FDCWD, path.characters(), times, 0) == -1) 244 err("failed to touch '{}': {}", path, strerror(errno)); 245 } else if (!no_create_file) { 246 int fd = TRY(Core::System::open(path, O_CREAT, 0100644)); 247 if (futimens(fd, times) == -1) 248 err("failed to touch '{}': {}", path, strerror(errno)); 249 TRY(Core::System::close(fd)); 250 } 251 } 252 return 0; 253}