From d350b9233a8d68dd1886443a8a50f634e37f5810 Mon Sep 17 00:00:00 2001 From: Tshaka Lekholoane Date: Sat, 25 Jan 2025 00:58:31 +0200 Subject: [PATCH] init: initial commit --- .clang-format | 2 + .github/dependabot.yaml | 6 ++ .github/workflows/ci.yaml | 21 ++++++ .gitignore | 56 ++++++++++++++++ LICENCE | 15 +++++ Makefile | 19 ++++++ README.md | 27 ++++++++ src/main.c | 130 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 276 insertions(+) create mode 100644 .clang-format create mode 100644 .github/dependabot.yaml create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 LICENCE create mode 100644 Makefile create mode 100644 README.md create mode 100644 src/main.c diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..1aadb6c --- /dev/null +++ b/.clang-format @@ -0,0 +1,2 @@ +ColumnLimit: 0 +PointerAlignment: Left diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..79fc83a --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..4d0c329 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,21 @@ +name: ci + +on: [pull_request, push] + +jobs: + check: + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + + - name: build + run: make CC=$(brew --prefix llvm@18)/bin/clang + + - name: run + run: | + touch x + bin/can x + if [[ -f x ]]; then + exit 1 + fi + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9caf8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# General. +.DS_Store +bin/ + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..f5c579f --- /dev/null +++ b/LICENCE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2023 Tshaka Lekholoane + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0ab5869 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +COMMIT = $(shell git rev-parse --short HEAD) +DATE = $(shell date -u +"%Y.%m.%d") +BUILD = $(shell printf "%s (%s)" "$(DATE)" "$(COMMIT)" ) +CC = cc +CFLAGS = -DCAN_BUILD="\"$(BUILD)\"" -O3 -Wall -Wextra -Wno-c++98-compat \ + -Wno-cast-function-type-strict -Wno-declaration-after-statement \ + -Wno-format-nonliteral -Wno-incompatible-pointer-types-discards-qualifiers \ + -Wno-poison-system-directories -Wno-vla -framework Foundation -march=native \ + -pedantic -std=c23 + +bin/can: src/main.o + mkdir -p bin + $(CC) $(CFLAGS) src/main.o -o bin/can + +src/main.o: + +.PHONY: clean +clean: + -rm bin/can src/main.o diff --git a/README.md b/README.md new file mode 100644 index 0000000..01aa17d --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# `can` + +![Continuous Integration](https://github.com/tshakalekholoane/can/actions/workflows/ci.yaml/badge.svg) + +`can` is a macOS command-line utility that provides an alternative to the `rm` command. Instead of permanently deleting files and directories, `can` moves them to the user's Trash, allowing for easy recovery if needed. + +## Usage + +``` +usage: can [-h | -V] [--] file ... +``` + +## Installation + +### Source + +The application can be built from source by cloning the repository and running the following commands which require working versions of [Make](https://www.gnu.org/software/make/) and a C compiler with C23 support. + +> [!WARNING] +> GCC 14 incorrectly uses `__STDC_VERSION__ == 202000` for C23 [^1], which will produce an error even when the `-std=c23` flag is set. + +```shell +git clone https://github.com/tshakalekholoane/can && cd can +make +``` + +[^1]: See https://stackoverflow.com/a/78582932. diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..5691a97 --- /dev/null +++ b/src/main.c @@ -0,0 +1,130 @@ +#ifndef __APPLE__ +#error "This program is intended to only run on macOS." +#endif + +#if __STDC_VERSION__ < 202000 +#error "This code requires C23 or later." +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifndef CAN_BUILD +#define CAN_BUILD "TIP" +#endif + +#define shift(n) \ + do { \ + argc -= (n); \ + argv += (n); \ + } while (false) + +#define unlikely(c) __builtin_expect(!!(c), false) + +static const char* usage = "usage: can [-h | -V] [--] file ..."; + +// Objective-C messaging primitives require that functions be cast to an +// appropriate function pointer type before being called [1]. +// +// [1]: https://tshaka.dev/a/7hq9i9 + +static id file_manager_default_manager(void) { + auto file_manager = objc_getClass("NSFileManager"); + typedef id (*send_type)(Class, SEL); + auto func = (send_type)objc_msgSend; + return func(file_manager, sel_registerName("defaultManager")); +} + +static id file_manager_string_with_file_system_representation(id self, const char string[static 1]) { + typedef id (*send_type)(id, SEL, const char*, unsigned long); + auto func = (send_type)objc_msgSend; + return func(self, sel_registerName("stringWithFileSystemRepresentation:length:"), string, strlen(string)); +} + +static bool file_manager_trash_item_at_url(id self, id url, id* err) { + typedef bool (*send_type)(id, SEL, id, id, id*); + auto func = (send_type)objc_msgSend; + return func(self, sel_registerName("trashItemAtURL:resultingItemURL:error:"), url, nullptr, err); +} + +static id error_localized_description(id self) { + typedef id (*send_type)(id, SEL); + auto func = (send_type)objc_msgSend; + return func(self, sel_registerName("localizedDescription")); +} + +static const char* string_utf8_string(id self) { + typedef const char* (*send_type)(id, SEL); + auto func = (send_type)objc_msgSend; + return func(self, sel_registerName("UTF8String")); +} + +static id url_file_url_with_path(id string) { + auto url = objc_getClass("NSURL"); + typedef id (*send_type)(Class, SEL, id); + auto func = (send_type)objc_msgSend; + return func(url, sel_registerName("fileURLWithPath:"), string); +} + +int main(int argc, char* argv[argc + 1]) { + int opt; + while ((opt = getopt(argc, argv, "hV")) != -1) { + switch (opt) { + case 'h': + printf("%s\n", usage); + return EXIT_SUCCESS; + case 'V': + printf("can %s\n", CAN_BUILD); + return EXIT_SUCCESS; + default: + fprintf(stderr, "%s\n", usage); + return EXIT_FAILURE; + } + } + if (unlikely(argc == 1)) { + fprintf(stderr, "%s\n", usage); + return EXIT_FAILURE; + } + shift(1); + + for (ssize_t i = 0; i < (ssize_t)argc; i++) { + auto name = argv[i]; + if (unlikely(strcmp(name, ".") == 0 || strcmp(name, "..") == 0 || strcmp(name, "/") == 0)) { + fprintf(stderr, "\"/\", \".\", and \"..\" may not be removed.\n"); + return EXIT_FAILURE; + } + } + + // Avoid using the root user's trash when invoked with sudo. + auto superuser = getenv("SUDO_USER"); + if (unlikely(superuser != nullptr)) { + auto entry = getpwnam(superuser); + if (unlikely(!entry || seteuid(entry->pw_uid))) { + fprintf(stderr, "%s\n", strerror(errno)); + return EXIT_FAILURE; + } + } + + // Ignore flag separator. + if (unlikely(strcmp(argv[0], "--") == 0)) + shift(1); + + auto exit_code = EXIT_SUCCESS; + auto file_manager = file_manager_default_manager(); + for (ssize_t i = 0; i < (ssize_t)argc; i++) { + auto path = file_manager_string_with_file_system_representation(file_manager, argv[i]); + auto url = url_file_url_with_path(path); + id err; + if (unlikely(!file_manager_trash_item_at_url(file_manager, url, &err))) { + auto description = error_localized_description(err); + fprintf(stderr, "%s\n", string_utf8_string(description)); + exit_code = EXIT_FAILURE; + } + } + return exit_code; +}