logo

utils

~/.local/bin tools and git-hooks git clone https://hacktivis.me/git/utils.git
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:

Acmd/touch.c178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest-cmd/Kyuafile1+
Atest-cmd/touch156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 +}