commit: 868b000f99063bfcb16d90193ca47a1625b5cea2
parent 523751119b5ca0b49d55fa9a464fff7167f6b316
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Sat, 3 Jun 2023 01:56:23 +0200
cmd/touch: New command
Diffstat:
A | cmd/touch.c | 178 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | test-cmd/Kyuafile | 1 | + |
A | test-cmd/touch | 156 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
3 files changed, 335 insertions(+), 0 deletions(-)
diff --git a/cmd/touch.c b/cmd/touch.c
@@ -0,0 +1,178 @@
+// Collection of Unix tools, comparable to coreutils
+// SPDX-FileCopyrightText: 2023 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
+
+#define _BSD_SOURCE // strptime, tm_gmtoff/tm_zone
+#include <fcntl.h> /* open */
+#include <stdbool.h> /* bool */
+#include <stdio.h> /* perror, sscanf */
+#include <stdlib.h> /* exit */
+#include <string.h> /* memset */
+#include <sys/stat.h> /* futimens, stat */
+#include <time.h> /* strptime, tm */
+#include <unistd.h> /* access */
+
+// Calls exit() on failure
+struct timespec
+iso_parse(char *arg)
+{
+ // YYYY-MM-DD[T ]hh:mm:SS([,\.]frac)?Z?
+ // Dammit Unixes why no nanoseconds in `struct tm` nor `strptime`
+ struct timespec time = {.tv_sec = 0, .tv_nsec = 0};
+ struct tm tm;
+ memset(&tm, 0, sizeof(tm));
+
+ // No %F in POSIX
+ char *s = strptime(arg, "%Y-%m-%d", &tm);
+
+ if(s[0] != 'T' && s[0] != ' ') exit(EXIT_FAILURE);
+ s++;
+
+ s = strptime(s, "%H:%M:%S", &tm);
+
+ if(s[0] == ',' || s[0] == '.')
+ {
+ float fraction = 0.0;
+ int parsed = 0;
+
+ if(s[0] == ',') s[0] = '.';
+
+ if(sscanf(s, "%f%n", &fraction, &parsed) < 1) exit(EXIT_FAILURE);
+
+ time.tv_nsec = fraction * 1000000000;
+ s += parsed;
+ }
+
+ if(s[0] == 'Z')
+ {
+ tm.tm_gmtoff = 0;
+ tm.tm_zone = "UTC";
+ }
+
+ time.tv_sec = mktime(&tm);
+ if(time.tv_sec == (time_t)-1)
+ {
+ perror("touch: mktime");
+ exit(EXIT_FAILURE);
+ }
+
+ return time;
+}
+
+int
+main(int argc, char *argv[])
+{
+ bool ch_atime = false, ch_mtime = false, no_create = false;
+ char *ref_file = NULL;
+ struct timespec times[2] = {
+ {.tv_sec = 0, .tv_nsec = UTIME_OMIT}, // access
+ {.tv_sec = 0, .tv_nsec = UTIME_OMIT} // modification
+ };
+ struct timespec target = {0, UTIME_NOW};
+
+ int c = 0;
+ while((c = getopt(argc, argv, ":acmr:t:d:")) != -1)
+ {
+ switch(c)
+ {
+ case 'a':
+ ch_atime = true;
+ break;
+ case 'c':
+ no_create = true;
+ break;
+ case 'm':
+ ch_mtime = true;
+ break;
+ case 'r':
+ ref_file = optarg;
+ break;
+ case 't': // [[CC]YY]MMDDhhmm[.SS]
+ // Too legacy of a format, too annoying to parse
+ fprintf(stderr, "touch: Option -d not supported, use -t\n");
+ return 1;
+ break;
+ case 'd':
+ target = iso_parse(optarg);
+ break;
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ // When neither -a nor -m are specified, change both
+ if(!ch_atime && !ch_mtime)
+ {
+ ch_atime = true;
+ ch_mtime = true;
+ }
+
+ if(ref_file == NULL)
+ {
+ if(ch_atime) times[0] = target;
+ if(ch_mtime) times[1] = target;
+ }
+ else
+ {
+ struct stat ref;
+
+ if(stat(ref_file, &ref) != 0)
+ {
+ perror("touch: stat");
+ return 1;
+ }
+
+ if(ch_atime)
+ {
+ times[0] = ref.st_atim;
+ }
+ if(ch_mtime)
+ {
+ times[1] = ref.st_mtim;
+ }
+ }
+
+ for(int i = 0; i < argc; i++)
+ {
+ if(access(argv[i], F_OK) != 0)
+ {
+ if(no_create)
+ {
+ // Undefined return value in POSIX
+ return 1;
+ }
+
+ // File doesn't exists
+ int fd = creat(argv[i], S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
+
+ if(fd == -1)
+ {
+ perror("touch: open");
+ return 1;
+ }
+
+ if(futimens(fd, times) != 0)
+ {
+ perror("touch: futimens");
+ return 1;
+ }
+
+ if(close(fd) != 0)
+ {
+ perror("touch: close");
+ return 1;
+ }
+ }
+ else
+ {
+ if(utimensat(AT_FDCWD, argv[i], times, 0) != 0)
+ {
+ perror("touch: utimensat");
+ return 1;
+ }
+ }
+ }
+
+ return 0;
+}
diff --git a/test-cmd/Kyuafile b/test-cmd/Kyuafile
@@ -30,6 +30,7 @@ atf_test_program{name="sizeof", required_files=basedir.."/cmd/sizeof", timeout=1
atf_test_program{name="sname", required_files=basedir.."/cmd/sname", timeout=1}
atf_test_program{name="strings", required_files=basedir.."/cmd/strings", timeout=1}
atf_test_program{name="tee", required_files=basedir.."/cmd/tee", timeout=1}
+atf_test_program{name="touch", required_files=basedir.."/cmd/touch", timeout=1}
atf_test_program{name="true", required_files=basedir.."/cmd/true", timeout=1}
atf_test_program{name="tty", required_files=basedir.."/cmd/tty", timeout=1}
atf_test_program{name="unlink", required_files=basedir.."/cmd/unlink", timeout=1}
diff --git a/test-cmd/touch b/test-cmd/touch
@@ -0,0 +1,156 @@
+#!/usr/bin/env atf-sh
+# SPDX-FileCopyrightText: 2023 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
+
+atf_test_case noargs
+noargs_body() {
+ atf_check touch -a ./foo
+ atf_check touch -m ./foo
+ atime="$(stat -c'%x' ./foo)"
+ mtime="$(stat -c'%y' ./foo)"
+
+ atf_check ../cmd/touch ./foo
+ atf_check -o "not-inline:${atime}\n" stat -c'%x' ./foo
+ atf_check -o "not-inline:${mtime}\n" stat -c'%y' ./foo
+}
+
+atf_test_case ref_noargs
+ref_noargs_body() {
+ atf_check touch -a ./foo
+ atf_check touch -m ./foo
+ atime="$(stat -c'%x' ./foo)"
+ mtime="$(stat -c'%y' ./foo)"
+
+ atf_check ../cmd/touch -r ../cmd/touch ./foo
+ atf_check -o "not-inline:${atime}\n" stat -c'%x' ./foo
+ atf_check -o "not-inline:${mtime}\n" stat -c'%y' ./foo
+ atf_check -o "inline:$(stat -c%x ../cmd/touch)\n" stat -c'%x' ./foo
+ atf_check -o "inline:$(stat -c%y ../cmd/touch)\n" stat -c'%y' ./foo
+}
+
+atf_test_case mtime
+mtime_body() {
+ atf_check touch -a ./foo
+ atf_check touch -m ./foo
+ atime="$(stat -c'%x' ./foo)"
+ mtime="$(stat -c'%y' ./foo)"
+
+ atf_check ../cmd/touch -m ./foo
+ atf_check -o "inline:${atime}\n" stat -c'%x' ./foo
+ atf_check -o "not-inline:${mtime}\n" stat -c'%y' ./foo
+}
+
+atf_test_case ref_mtime
+ref_mtime_body() {
+ atf_check touch -a ./foo
+ atf_check touch -m ./foo
+ atime="$(stat -c'%x' ./foo)"
+ mtime="$(stat -c'%y' ./foo)"
+
+ atf_check ../cmd/touch -m -r ../cmd/touch ./foo
+ atf_check -o "inline:${atime}\n" stat -c'%x' ./foo
+ atf_check -o "not-inline:${mtime}\n" stat -c'%y' ./foo
+ atf_check -o "not-inline:$(stat -c%x ../cmd/touch)\n" stat -c'%x' ./foo
+ atf_check -o "inline:$(stat -c%y ../cmd/touch)\n" stat -c'%y' ./foo
+}
+
+atf_test_case atime
+atime_body() {
+ atf_check touch -a ./foo
+ atf_check touch -m ./foo
+ atime="$(stat -c'%x' ./foo)"
+ mtime="$(stat -c'%y' ./foo)"
+
+ atf_check ../cmd/touch -a ./foo
+ atf_check -o "not-inline:${atime}\n" stat -c'%x' ./foo
+ atf_check -o "inline:${mtime}\n" stat -c'%y' ./foo
+}
+
+atf_test_case ref_atime
+ref_atime_body() {
+ atf_check touch -a ./foo
+ atf_check touch -m ./foo
+ atime="$(stat -c'%x' ./foo)"
+ mtime="$(stat -c'%y' ./foo)"
+
+ atf_check ../cmd/touch -a -r ../cmd/touch ./foo
+ atf_check -o "not-inline:${atime}\n" stat -c'%x' ./foo
+ atf_check -o "inline:${mtime}\n" stat -c'%y' ./foo
+ atf_check -o "inline:$(stat -c%x ../cmd/touch)\n" stat -c'%x' ./foo
+ atf_check -o "not-inline:$(stat -c%y ../cmd/touch)\n" stat -c'%y' ./foo
+}
+
+atf_test_case amtime
+amtime_body() {
+ atf_check touch -a ./foo
+ atf_check touch -m ./foo
+ atime="$(stat -c'%x' ./foo)"
+ mtime="$(stat -c'%y' ./foo)"
+
+ atf_check ../cmd/touch -a -m ./foo
+ atf_check -o "not-inline:${atime}\n" stat -c'%x' ./foo
+ atf_check -o "not-inline:${mtime}\n" stat -c'%y' ./foo
+}
+
+atf_test_case ref_amtime
+ref_amtime_body() {
+ atf_check touch -a ./foo
+ atf_check touch -m ./foo
+ atime="$(stat -c'%x' ./foo)"
+ mtime="$(stat -c'%y' ./foo)"
+
+ atf_check ../cmd/touch -a -m -r ../cmd/touch ./foo
+ atf_check -o "not-inline:${atime}\n" stat -c'%x' ./foo
+ atf_check -o "not-inline:${mtime}\n" stat -c'%y' ./foo
+ atf_check -o "inline:$(stat -c%x ../cmd/touch)\n" stat -c'%x' ./foo
+ atf_check -o "inline:$(stat -c%y ../cmd/touch)\n" stat -c'%y' ./foo
+}
+
+atf_test_case optd
+optd_body() {
+ atf_check touch -a ./foo
+ atf_check touch -m ./foo
+ atime="$(stat -c'%x' ./foo)"
+ mtime="$(stat -c'%y' ./foo)"
+
+ unset TZ
+
+ atf_check ../cmd/touch -d 2003-06-02T13:37:42Z ./foo
+ atf_check -o "not-inline:${atime}\n" stat -c'%x' ./foo
+ atf_check -o "not-inline:${mtime}\n" stat -c'%y' ./foo
+ atf_check -o 'match:^2003-06-02[T ]13:37:42(\.0+)? ?(Z|[\+\-]00:?00)$' stat -c'%x' ./foo
+ atf_check -o 'match:^2003-06-02[T ]13:37:42(\.0+)? ?(Z|[\+\-]00:?00)$' stat -c'%y' ./foo
+}
+
+atf_test_case optd_frac
+optd_frac_body() {
+ atf_check touch -a ./foo
+ atf_check touch -m ./foo
+ atime="$(stat -c'%x' ./foo)"
+ mtime="$(stat -c'%y' ./foo)"
+
+ unset TZ
+
+ atf_check ../cmd/touch -d 2003-06-02T13:37:42.713Z ./foo
+ atf_check -o "not-inline:${atime}\n" stat -c'%x' ./foo
+ atf_check -o "not-inline:${mtime}\n" stat -c'%y' ./foo
+ atf_check -o 'match:^2003-06-02[T ]13:37:42.7130+ ?(Z|[\+\-]00:?00)$' stat -c'%x' ./foo
+ atf_check -o 'match:^2003-06-02[T ]13:37:42.7130+ ?(Z|[\+\-]00:?00)$' stat -c'%y' ./foo
+}
+
+atf_init_test_cases() {
+ cd "$(atf_get_srcdir)" || exit 1
+
+ atf_add_test_case noargs
+ atf_add_test_case atime
+ atf_add_test_case mtime
+ atf_add_test_case amtime
+
+ atf_add_test_case ref_noargs
+ atf_add_test_case ref_atime
+ atf_add_test_case ref_mtime
+ atf_add_test_case ref_amtime
+
+ atf_add_test_case optd
+ atf_add_test_case optd_frac
+}