Serenity Operating System
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}