mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 19:22:45 +00:00 
			
		
		
		
	 a96f307af1
			
		
	
	
		a96f307af1
		
	
	
	
	
		
			
			`inline` already assigns vague linkage, so there's no need to also assign per-TU linkage. Allows the linker to dedup these functions across TUs (and is almost always just the Right Thing to do in C++ -- this ain't C).
		
			
				
	
	
		
			253 lines
		
	
	
	
		
			9.6 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			253 lines
		
	
	
	
		
			9.6 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
|  * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
 | |
|  * Copyright (c) 2022, Ariel Don <ariel@arieldon.com>
 | |
|  *
 | |
|  * SPDX-License-Identifier: BSD-2-Clause
 | |
|  */
 | |
| 
 | |
| #include <AK/CheckedFormatString.h>
 | |
| #include <AK/GenericLexer.h>
 | |
| #include <AK/Time.h>
 | |
| #include <LibCore/ArgsParser.h>
 | |
| #include <LibCore/File.h>
 | |
| #include <LibCore/System.h>
 | |
| #include <LibMain/Main.h>
 | |
| #include <LibTimeZone/TimeZone.h>
 | |
| #include <ctype.h>
 | |
| #include <errno.h>
 | |
| #include <fcntl.h>
 | |
| #include <stdio.h>
 | |
| #include <stdlib.h>
 | |
| #include <sys/stat.h>
 | |
| #include <time.h>
 | |
| 
 | |
| static DeprecatedString program_name;
 | |
| 
 | |
| template<typename... Parameters>
 | |
| [[noreturn]] static void err(CheckedFormatString<Parameters...>&& fmtstr, Parameters const&... parameters)
 | |
| {
 | |
|     warn("{}: ", program_name);
 | |
|     warnln(move(fmtstr), parameters...);
 | |
|     exit(1);
 | |
| }
 | |
| 
 | |
| inline bool validate_timestamp(unsigned year, unsigned month, unsigned day, unsigned hour, unsigned minute, unsigned second)
 | |
| {
 | |
|     return (year >= 1970) && (month >= 1 && month <= 12) && (day >= 1 && day <= static_cast<unsigned>(days_in_month(year, month))) && (hour <= 23) && (minute <= 59) && (second <= 59);
 | |
| }
 | |
| 
 | |
| static void parse_time(StringView input_time, timespec& atime, timespec& mtime)
 | |
| {
 | |
|     // Parse [[CC]YY]MMDDhhmm[.SS] format, where brackets signify optional
 | |
|     // parameters.
 | |
|     if (input_time.length() < 8)
 | |
|         err("invalid time format '{}' -- too short", input_time);
 | |
|     else if (input_time.length() > 15)
 | |
|         err("invalid time format '{}' -- too long", input_time);
 | |
| 
 | |
|     Vector<u8> parameters;
 | |
|     GenericLexer lexer(input_time);
 | |
|     unsigned year, month, day, hour, minute, second;
 | |
| 
 | |
|     auto lex_number = [&]() {
 | |
|         auto literal = lexer.consume(2);
 | |
|         if (literal.length() < 2)
 | |
|             err("invalid time format '{}' -- expected 2 digits per parameter", input_time);
 | |
|         auto maybe_parameter = literal.to_uint();
 | |
|         if (maybe_parameter.has_value())
 | |
|             parameters.append(maybe_parameter.value());
 | |
|         else
 | |
|             err("invalid time format '{}'", input_time);
 | |
|     };
 | |
| 
 | |
|     while (!lexer.is_eof() && lexer.next_is(isdigit))
 | |
|         lex_number();
 | |
|     if (parameters.size() > 6)
 | |
|         err("invalid time format '{}' -- too many parameters", input_time);
 | |
| 
 | |
|     if (lexer.consume_specific('.')) {
 | |
|         lex_number();
 | |
|         second = parameters.take_last();
 | |
|     } else {
 | |
|         second = 0;
 | |
|     }
 | |
| 
 | |
|     auto current_year = seconds_since_epoch_to_year(time(nullptr));
 | |
|     auto current_century = current_year / 100;
 | |
|     if (parameters.size() == 6)
 | |
|         year = parameters.take_first() * 100 + parameters.take_first();
 | |
|     else if (parameters.size() == 5)
 | |
|         year = current_century * 100 + parameters.take_first();
 | |
|     else
 | |
|         year = current_year;
 | |
| 
 | |
|     minute = parameters.take_last();
 | |
|     hour = parameters.take_last();
 | |
|     day = parameters.take_last();
 | |
|     month = parameters.take_last();
 | |
| 
 | |
|     if (validate_timestamp(year, month, day, hour, minute, second))
 | |
|         atime = mtime = AK::Time::from_timestamp(year, month, day, hour, minute, second, 0).to_timespec();
 | |
|     else
 | |
|         err("invalid time format '{}'", input_time);
 | |
| }
 | |
| 
 | |
| static void parse_datetime(StringView input_datetime, timespec& atime, timespec& mtime)
 | |
| {
 | |
|     // Parse YYYY-MM-DDThh:mm:SS[.frac][tz] or YYYY-MM-DDThh:mm:SS[,frac][tz]
 | |
|     // formats, where brackets signify optional parameters.
 | |
|     GenericLexer lexer(input_datetime);
 | |
|     unsigned year, month, day, hour, minute, second, millisecond;
 | |
|     StringView time_zone;
 | |
| 
 | |
|     auto lex_number = [&](unsigned& value, size_t n) {
 | |
|         auto maybe_value = lexer.consume(n).to_uint();
 | |
|         if (!maybe_value.has_value())
 | |
|             err("invalid datetime format '{}' -- expected number at index {}", input_datetime, lexer.tell());
 | |
|         else
 | |
|             value = maybe_value.value();
 | |
|     };
 | |
| 
 | |
|     lex_number(year, 4);
 | |
|     if (!lexer.consume_specific('-'))
 | |
|         err("invalid datetime format '{}' -- expected '-' after year", input_datetime);
 | |
|     lex_number(month, 2);
 | |
|     if (!lexer.consume_specific('-'))
 | |
|         err("invalid datetime format '{}' -- expected '-' after month", input_datetime);
 | |
|     lex_number(day, 2);
 | |
| 
 | |
|     // Parse the time designator -- a single 'T' or ' ' according to POSIX.
 | |
|     if (!lexer.consume_specific('T') && !lexer.consume_specific(' '))
 | |
|         err("invalid datetime format '{}' -- expected 'T' or ' ' for time designator", input_datetime);
 | |
| 
 | |
|     lex_number(hour, 2);
 | |
|     if (!lexer.consume_specific(':'))
 | |
|         err("invalid datetime format '{}' -- expected ':' after hour", input_datetime);
 | |
|     lex_number(minute, 2);
 | |
|     if (!lexer.consume_specific(':'))
 | |
|         err("invalid datetime format '{}' -- expected ':' after minute", input_datetime);
 | |
|     lex_number(second, 2);
 | |
| 
 | |
|     millisecond = 0;
 | |
|     if (!lexer.is_eof()) {
 | |
|         if (lexer.consume_specific(',') || lexer.consume_specific('.')) {
 | |
|             auto fractional_second = lexer.consume_while(isdigit);
 | |
|             if (fractional_second.is_empty())
 | |
|                 err("invalid datetime format '{}' -- expected floating seconds", input_datetime);
 | |
|             for (u8 i = 0; i < 3 && i < fractional_second.length(); ++i) {
 | |
|                 unsigned n = fractional_second[i] - '0';
 | |
|                 switch (i) {
 | |
|                 case 0:
 | |
|                     millisecond += 100 * n;
 | |
|                     break;
 | |
|                 case 1:
 | |
|                     millisecond += 10 * n;
 | |
|                     break;
 | |
|                 case 2:
 | |
|                     millisecond += n;
 | |
|                     break;
 | |
|                 default:
 | |
|                     VERIFY_NOT_REACHED();
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         time_zone = lexer.consume_all();
 | |
|         if (!time_zone.is_empty() && time_zone != "Z")
 | |
|             err("invalid datetime format '{}' -- failed to parse time zone", input_datetime);
 | |
|     }
 | |
| 
 | |
|     if (validate_timestamp(year, month, day, hour, minute, second)) {
 | |
|         auto timestamp = AK::Time::from_timestamp(year, month, day, hour, minute, second, millisecond);
 | |
|         auto time = timestamp.to_timespec();
 | |
|         if (time_zone.is_empty() && TimeZone::system_time_zone() != "UTC") {
 | |
|             auto offset = TimeZone::get_time_zone_offset(TimeZone::system_time_zone(), timestamp);
 | |
|             if (offset.has_value())
 | |
|                 time.tv_sec -= offset.value().seconds;
 | |
|             else
 | |
|                 err("failed to get the system time zone");
 | |
|         }
 | |
|         atime = mtime = time;
 | |
|     } else {
 | |
|         err("invalid datetime format '{}'", input_datetime);
 | |
|     }
 | |
| }
 | |
| 
 | |
| static void reference_time(StringView reference_path, timespec& atime, timespec& mtime)
 | |
| {
 | |
|     auto maybe_buffer = Core::System::stat(reference_path);
 | |
|     if (maybe_buffer.is_error())
 | |
|         err("failed to reference times of '{}': {}", reference_path, maybe_buffer.release_error());
 | |
|     auto buffer = maybe_buffer.release_value();
 | |
|     atime.tv_sec = buffer.st_atime;
 | |
|     atime.tv_nsec = buffer.st_atim.tv_nsec;
 | |
|     mtime.tv_sec = buffer.st_mtime;
 | |
|     mtime.tv_nsec = buffer.st_mtim.tv_nsec;
 | |
| }
 | |
| 
 | |
| ErrorOr<int> serenity_main(Main::Arguments arguments)
 | |
| {
 | |
|     TRY(Core::System::pledge("stdio rpath cpath fattr"));
 | |
| 
 | |
|     program_name = arguments.strings[0];
 | |
| 
 | |
|     Vector<DeprecatedString> paths;
 | |
| 
 | |
|     timespec times[2];
 | |
|     auto& atime = times[0];
 | |
|     auto& mtime = times[1];
 | |
| 
 | |
|     bool update_atime = false;
 | |
|     bool update_mtime = false;
 | |
|     bool no_create_file = false;
 | |
| 
 | |
|     DeprecatedString input_datetime = "";
 | |
|     DeprecatedString input_time = "";
 | |
|     DeprecatedString reference_path = "";
 | |
| 
 | |
|     Core::ArgsParser args_parser;
 | |
|     args_parser.set_general_help("Create a file or update file access time and/or modification time.");
 | |
|     args_parser.add_ignored(nullptr, 'f');
 | |
|     args_parser.add_option(update_atime, "Change access time of file", nullptr, 'a');
 | |
|     args_parser.add_option(no_create_file, "Do not create a file if it does not exist", nullptr, 'c');
 | |
|     args_parser.add_option(update_mtime, "Change modification time of file", nullptr, 'm');
 | |
|     args_parser.add_option(input_datetime, "Use specified datetime instead of current time", nullptr, 'd', "datetime");
 | |
|     args_parser.add_option(input_time, "Use specified time instead of current time", nullptr, 't', "time");
 | |
|     args_parser.add_option(reference_path, "Use time of file specified by reference path instead of current time", nullptr, 'r', "reference");
 | |
|     args_parser.add_positional_argument(paths, "Files to touch", "path", Core::ArgsParser::Required::Yes);
 | |
|     args_parser.parse(arguments);
 | |
| 
 | |
|     if (input_datetime.is_empty() + input_time.is_empty() + reference_path.is_empty() < 2)
 | |
|         err("cannot specify a time with more than one option");
 | |
| 
 | |
|     if (!input_datetime.is_empty())
 | |
|         parse_datetime(input_datetime, atime, mtime);
 | |
|     else if (!input_time.is_empty())
 | |
|         parse_time(input_time, atime, mtime);
 | |
|     else if (!reference_path.is_empty())
 | |
|         reference_time(reference_path, atime, mtime);
 | |
|     else
 | |
|         atime.tv_nsec = mtime.tv_nsec = UTIME_NOW;
 | |
| 
 | |
|     // According to POSIX, if neither -a nor -m are specified, then the program
 | |
|     // should behave as if both are specified.
 | |
|     if (!update_atime && !update_mtime)
 | |
|         update_atime = update_mtime = true;
 | |
|     if (update_atime && !update_mtime)
 | |
|         mtime.tv_nsec = UTIME_OMIT;
 | |
|     if (update_mtime && !update_atime)
 | |
|         atime.tv_nsec = UTIME_OMIT;
 | |
| 
 | |
|     for (auto path : paths) {
 | |
|         if (Core::File::exists(path)) {
 | |
|             if (utimensat(AT_FDCWD, path.characters(), times, 0) == -1)
 | |
|                 err("failed to touch '{}': {}", path, strerror(errno));
 | |
|         } else if (!no_create_file) {
 | |
|             int fd = TRY(Core::System::open(path, O_CREAT, 0100644));
 | |
|             if (futimens(fd, times) == -1)
 | |
|                 err("failed to touch '{}': {}", path, strerror(errno));
 | |
|             TRY(Core::System::close(fd));
 | |
|         }
 | |
|     }
 | |
|     return 0;
 | |
| }
 |