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..ac5cd99 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,15 @@ +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: ./bin/can -V 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..aa68f7f --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +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 + +.PHONY: all clean + +all: + mkdir -p bin/ + $(CC) $(CFLAGS) -o bin/can src/main.c + +clean: + rm -rf bin/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec4b5bc --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# `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. + +```shell +git clone https://github.com/tshakalekholoane/can && cd can +make +``` diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..26d045b --- /dev/null +++ b/src/main.c @@ -0,0 +1,125 @@ +#ifndef __APPLE__ +#error "This program is intended to only run on macOS." +#endif + +#if __STDC_VERSION__ < 202311L +#error "This code requires C23 or later." +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifndef CAN_BUILD +#define CAN_BUILD "TIP" +#endif + +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://github.com/apple-oss-distributions/objc4/blob/89543e2c0f67d38ca5211cea33f42c51500287d5/runtime/message.h#L52-L53 + +static struct objc_object* file_manager_default_manager(void) { + auto file_manager = objc_getClass("NSFileManager"); + typedef struct objc_object* (*send_type)(struct objc_class*, struct objc_selector*); + auto func = (send_type)objc_msgSend; + return func(file_manager, sel_registerName("defaultManager")); +} + +static struct objc_object* file_manager_string_with_file_system_representation(struct objc_object* self, const char string[static 1]) { + typedef struct objc_object* (*send_type)(struct objc_object*, struct objc_selector*, 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(struct objc_object* self, struct objc_object* url, struct objc_object** error) { + typedef bool (*send_type)(struct objc_object*, struct objc_selector*, struct objc_object*, struct objc_object*, struct objc_object**); + auto func = (send_type)objc_msgSend; + return func(self, sel_registerName("trashItemAtURL:resultingItemURL:error:"), url, nullptr, error); +} + +static struct objc_object* error_localized_description(struct objc_object* self) { + typedef struct objc_object* (*send_type)(struct objc_object*, struct objc_selector*); + auto func = (send_type)objc_msgSend; + return func(self, sel_registerName("localizedDescription")); +} + +static const char* string_utf8_string(struct objc_object* self) { + typedef const char* (*send_type)(struct objc_object*, struct objc_selector*); + auto func = (send_type)objc_msgSend; + return func(self, sel_registerName("UTF8String")); +} + +static struct objc_object* url_file_url_with_path(struct objc_object* string) { + auto url = objc_getClass("NSURL"); + typedef struct objc_object* (*send_type)(struct objc_class*, struct objc_selector*, struct objc_object*); + 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 (__builtin_expect(argc == 1, false)) { + fprintf(stderr, "%s\n", usage); + return EXIT_FAILURE; + } + argc--; + argv++; + + for (ssize_t i = 0; i < (ssize_t)argc; i++) { + auto name = argv[i]; + if (__builtin_expect(strcmp(name, ".") == 0 || strcmp(name, "..") == 0 || strcmp(name, "/") == 0, false)) { + 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 (__builtin_expect(superuser != nullptr, false)) { + auto entry = getpwnam(superuser); + if (__builtin_expect(!entry || seteuid(entry->pw_uid), false)) { + fprintf(stderr, "%s\n", strerror(errno)); + return EXIT_FAILURE; + } + } + + // Ignore flag separator. + if (__builtin_expect(strcmp(argv[0], "--") == 0, false)) { + argc--; + argv++; + } + + 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); + struct objc_object* err; + if (__builtin_expect(!file_manager_trash_item_at_url(file_manager, url, &err), false)) { + auto description = error_localized_description(err); + fprintf(stderr, "%s\n", string_utf8_string(description)); + exit_code = EXIT_FAILURE; + } + } + return exit_code; +}