suploader

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | LICENSE

commit 2ad60da1ede63846978d1276790f9dc3b60d3961
Author: Kris Yotam <krisyotam@protonmail.com>
Date:   Mon, 16 Feb 2026 02:40:52 -0600

Initial commit: suploader - Simple Uploader

Suckless C tool that uploads URLs to web archive services
(Internet Archive, archive.ph, Wikiwix). Reads URLs from
a file or stdin with configurable rate limiting, random
selection, and daemon mode.

Diffstat:
A.claude/CLAUDE.md | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A.gitignore | 2++
ALICENSE | 21+++++++++++++++++++++
AMakefile | 45+++++++++++++++++++++++++++++++++++++++++++++
Aconfig.h | 29+++++++++++++++++++++++++++++
Aservices.c | 285+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aservices.h | 25+++++++++++++++++++++++++
Asuploader.c | 339+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autil.c | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autil.h | 21+++++++++++++++++++++
10 files changed, 998 insertions(+), 0 deletions(-)

diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md @@ -0,0 +1,141 @@ +# suploader — CLAUDE.md + +## Project + +suploader (Simple Uploader) is a suckless tool that uploads URLs to web +archive services. It reads a file of URLs (one per line) or stdin and +submits each to the Internet Archive (Wayback Machine), archive.today +(archive.ph), and Wikiwix. Supports configurable rate limiting, random +URL selection, and daemon mode for continuous background archiving. + +## Coding Standards — Suckless C Style + +All code in this project MUST follow the suckless.org coding style: + +### Language +- C99 (ISO/IEC 9899:1999), no extensions +- POSIX.1-2008 (`_POSIX_C_SOURCE 200809L`) + +### Indentation & Whitespace +- Tabs for indentation (1 tab = 1 level) +- Spaces for alignment only, never for indentation +- No tabs except at the beginning of a line +- Maximum line length: 79 characters + +### Comments +- Use `/* */` only, never `//` +- Comment fallthrough cases in switch statements + +### Variables +- All declarations at the top of the block +- Pointer `*` adjacent to variable name: `char *p`, not `char* p` +- No C99 `bool`; use `int` (0/1) +- Global/static variables not used outside TU must be `static` + +### Functions +- Return type on its own line +- Function name at column 0 on next line (enables `grep ^funcname`) +- Opening `{` on its own line for functions +- Functions not used outside their file: `static` + +```c +static void +usage(void) +{ + fprintf(stderr, "usage: suploader [-v] [-d secs] file\n"); + exit(1); +} +``` + +### Braces +- Opening `{` on same line for control flow (if, for, while, switch) +- Closing `}` on its own line unless continuing (else, do-while) +- Use braces even for single statements when sibling branches use them + +### Naming +- lowercase_with_underscores for functions and variables +- UPPERCASE for macros and constants +- CamelCase for typedef'd struct types +- No `_t` suffix (reserved by POSIX) +- Prefix module functions with module name + +### Control Flow +- Space after `if`, `for`, `while`, `switch` +- No space after `(` or before `)` +- Use `goto` for cleanup/unwind, not nested ifs +- Return/exit early on failure +- Test against 0, not -1: `if (func() < 0)` + +### Error Handling +- All allocation checked; goto cleanup on failure +- `die()` for fatal errors (prints message, exits) +- `warn()` for recoverable errors (prints, continues) + +### File Organization Order +1. License header +2. System includes (alphabetical) +3. Local includes +4. Macros +5. Type definitions +6. Function declarations +7. Global variables +8. Function definitions (same order as declarations) + +### Headers +- System headers first, alphabetical +- Local headers after blank line +- No cyclic dependencies +- Include only what is needed + +## Architecture + +### Module Layout + +| Module | Prefix | File | Responsibility | +|--------|--------|------|----------------| +| Main | — | suploader.c | Entry point, URL file I/O, daemon loop | +| Services | `svc_` | services.c | Archive service submission (IA, archive.ph, Wikiwix) | +| Utilities | `die`, `warn`, `x*` | util.c | Memory wrappers, string ops, error handling | +| Config | — | config.h | Compile-time constants (timeouts, delays, user agent) | + +### Architecture Rules +- **Separate compilation.** Every .c file compiles independently. +- **No dynamic loading.** All features compiled in. +- **libcurl only.** Single external dependency for HTTP. +- **Fire-and-forget.** Submit and move on; no verification polling. +- **Text file interface.** One URL per line, Unix-philosophy I/O. + +## Build + +```sh +make # build suploader binary +make clean # remove build artifacts +make install # install to /usr/local/bin +``` + +Dependencies: `libcurl` (via pkg-config) + +## Usage + +```sh +# Upload URLs from a file +suploader urls.txt + +# Read from stdin +cat urls.txt | suploader - + +# Daemon mode (loop forever, process one URL per cycle) +suploader -D urls.txt + +# Custom delay between submissions (seconds) +suploader -d 60 urls.txt + +# Verbose output +suploader -v urls.txt +``` + +## Git Conventions + +- No `Co-Authored-By: Claude` lines +- Commit messages: imperative, <72 chars, no period +- One logical change per commit diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +suploader +*.o diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +MIT/X Consortium License + +(c) 2026 Kris Yotam <krisyotam@proton.me> + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile @@ -0,0 +1,45 @@ +# suploader - Simple Uploader +# See LICENSE file for copyright and license details. + +VERSION = 0.1.0 + +# paths +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man + +# includes and libs +INCS = `pkg-config --cflags libcurl` +LIBS = `pkg-config --libs libcurl` + +# flags +CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_POSIX_C_SOURCE=200809L -DVERSION=\"$(VERSION)\" +CFLAGS = -std=c99 -pedantic -Wall -Wextra -Os $(INCS) $(CPPFLAGS) +LDFLAGS = $(LIBS) + +# compiler +CC = cc + +# sources +SRC = suploader.c services.c util.c +OBJ = $(SRC:.c=.o) + +all: suploader + +.c.o: + $(CC) $(CFLAGS) -c $< + +suploader: $(OBJ) + $(CC) -o $@ $(OBJ) $(LDFLAGS) + +clean: + rm -f suploader $(OBJ) + +install: all + mkdir -p $(DESTDIR)$(PREFIX)/bin + cp -f suploader $(DESTDIR)$(PREFIX)/bin + chmod 755 $(DESTDIR)$(PREFIX)/bin/suploader + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/suploader + +.PHONY: all clean install uninstall diff --git a/config.h b/config.h @@ -0,0 +1,29 @@ +/* See LICENSE file for copyright and license details. + * suploader - Simple Uploader + * configuration header + */ + +#ifndef CONFIG_H +#define CONFIG_H + +/* Program metadata */ +#define PROG_NAME "suploader" +#define PROG_VERSION "0.1.0" + +/* Network settings */ +#define USER_AGENT "suploader/0.1 (+https://krisyotam.com)" +#define CONNECT_TIMEOUT 30L +#define REQUEST_TIMEOUT 60L +#define MAX_REDIRECTS 10L + +/* Rate limiting */ +#define DEFAULT_DELAY 48 /* seconds between submissions */ + +/* Retry settings */ +#define MAX_RETRIES 3 +#define RETRY_BASE 2 /* base seconds for exponential backoff */ + +/* URL limits */ +#define MAX_URL_LEN 4096 + +#endif /* CONFIG_H */ diff --git a/services.c b/services.c @@ -0,0 +1,285 @@ +/* See LICENSE file for copyright and license details. */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <curl/curl.h> + +#include "config.h" +#include "services.h" +#include "util.h" + +static CURL *curl_handle = NULL; + +/* Discard response body */ +static size_t +discard_cb(void *contents, size_t size, size_t nmemb, void *userp) +{ + (void)contents; + (void)userp; + return size * nmemb; +} + +static int +is_transient(long code) +{ + return code == 429 || code == 500 || code == 502 || + code == 503 || code == 504; +} + +/* Perform a GET request, return HTTP status or -1 on error */ +static long +http_get(const char *url) +{ + CURLcode res; + long status; + int attempt; + + for (attempt = 0; attempt < MAX_RETRIES; attempt++) { + if (attempt > 0) { + unsigned int delay; + + delay = RETRY_BASE * (1 << (attempt - 1)); + sleep(delay); + } + + curl_easy_reset(curl_handle); + curl_easy_setopt(curl_handle, CURLOPT_URL, url); + curl_easy_setopt(curl_handle, + CURLOPT_WRITEFUNCTION, discard_cb); + curl_easy_setopt(curl_handle, + CURLOPT_USERAGENT, USER_AGENT); + curl_easy_setopt(curl_handle, + CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl_handle, + CURLOPT_MAXREDIRS, MAX_REDIRECTS); + curl_easy_setopt(curl_handle, + CURLOPT_CONNECTTIMEOUT, CONNECT_TIMEOUT); + curl_easy_setopt(curl_handle, + CURLOPT_TIMEOUT, REQUEST_TIMEOUT); + curl_easy_setopt(curl_handle, + CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl_handle, + CURLOPT_SSL_VERIFYHOST, 2L); + + res = curl_easy_perform(curl_handle); + if (res != CURLE_OK) + continue; + + curl_easy_getinfo(curl_handle, + CURLINFO_RESPONSE_CODE, &status); + + if (is_transient(status)) + continue; + + return status; + } + return -1; +} + +/* Perform a POST request, return HTTP status or -1 on error */ +static long +http_post(const char *url, const char *postdata) +{ + CURLcode res; + long status; + int attempt; + + for (attempt = 0; attempt < MAX_RETRIES; attempt++) { + if (attempt > 0) { + unsigned int delay; + + delay = RETRY_BASE * (1 << (attempt - 1)); + sleep(delay); + } + + curl_easy_reset(curl_handle); + curl_easy_setopt(curl_handle, CURLOPT_URL, url); + curl_easy_setopt(curl_handle, + CURLOPT_POSTFIELDS, postdata); + curl_easy_setopt(curl_handle, + CURLOPT_WRITEFUNCTION, discard_cb); + curl_easy_setopt(curl_handle, + CURLOPT_USERAGENT, USER_AGENT); + curl_easy_setopt(curl_handle, + CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl_handle, + CURLOPT_MAXREDIRS, MAX_REDIRECTS); + curl_easy_setopt(curl_handle, + CURLOPT_CONNECTTIMEOUT, CONNECT_TIMEOUT); + curl_easy_setopt(curl_handle, + CURLOPT_TIMEOUT, REQUEST_TIMEOUT); + curl_easy_setopt(curl_handle, + CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl_handle, + CURLOPT_SSL_VERIFYHOST, 2L); + + res = curl_easy_perform(curl_handle); + if (res != CURLE_OK) + continue; + + curl_easy_getinfo(curl_handle, + CURLINFO_RESPONSE_CODE, &status); + + if (is_transient(status)) + continue; + + return status; + } + return -1; +} + +void +svc_init(void) +{ + curl_global_init(CURL_GLOBAL_ALL); + curl_handle = curl_easy_init(); + if (!curl_handle) + die("curl_easy_init failed"); +} + +void +svc_cleanup(void) +{ + if (curl_handle) { + curl_easy_cleanup(curl_handle); + curl_handle = NULL; + } + curl_global_cleanup(); +} + +int +svc_submit_ia(const char *url, int verbose) +{ + char *save_url; + size_t len; + long status; + + len = strlen("https://web.archive.org/save/") + + strlen(url) + 1; + save_url = xmalloc(len); + snprintf(save_url, len, + "https://web.archive.org/save/%s", url); + + if (verbose) + fprintf(stderr, " IA: %s\n", save_url); + + status = http_get(save_url); + free(save_url); + + if (status >= 200 && status < 400) { + if (verbose) + fprintf(stderr, " IA: OK (%ld)\n", status); + return 0; + } + + warn("IA: HTTP %ld for %s", status, url); + return -1; +} + +int +svc_submit_archiveph(const char *url, int verbose) +{ + char *postdata, *escaped; + size_t len; + long status; + + escaped = curl_easy_escape(curl_handle, url, 0); + if (!escaped) { + warn("archive.ph: URL escape failed for %s", url); + return -1; + } + + len = strlen("url=") + strlen(escaped) + + strlen("&submit=") + 1; + postdata = xmalloc(len); + snprintf(postdata, len, "url=%s&submit=", escaped); + curl_free(escaped); + + if (verbose) + fprintf(stderr, " archive.ph: submitting %s\n", + url); + + status = http_post("https://archive.ph/submit/", postdata); + free(postdata); + + if (status >= 200 && status < 400) { + if (verbose) + fprintf(stderr, + " archive.ph: OK (%ld)\n", status); + return 0; + } + + /* archive.ph returns 3xx redirect on success */ + if (status >= 300 && status < 400) { + if (verbose) + fprintf(stderr, + " archive.ph: OK (redirect)\n"); + return 0; + } + + warn("archive.ph: HTTP %ld for %s", status, url); + return -1; +} + +int +svc_submit_wikiwix(const char *url, int verbose) +{ + char *cache_url; + size_t len; + long status; + + len = strlen("https://archive.wikiwix.com/cache/?url=") + + strlen(url) + 1; + cache_url = xmalloc(len); + snprintf(cache_url, len, + "https://archive.wikiwix.com/cache/?url=%s", url); + + if (verbose) + fprintf(stderr, " Wikiwix: %s\n", cache_url); + + status = http_get(cache_url); + free(cache_url); + + if (status >= 200 && status < 400) { + if (verbose) + fprintf(stderr, + " Wikiwix: OK (%ld)\n", status); + return 0; + } + + warn("Wikiwix: HTTP %ld for %s", status, url); + return -1; +} + +int +svc_submit(const char *url, int services, int verbose) +{ + int ok; + + ok = 0; + + /* Skip .onion URLs - archive services can't reach them */ + if (strstr(url, ".onion/") || strstr(url, ".onion:")) { + if (verbose) + fprintf(stderr, + " skip: .onion URL\n"); + return 0; + } + + if (services & SVC_IA) { + if (svc_submit_ia(url, verbose) == 0) + ok++; + } + if (services & SVC_WIKIWIX) { + if (svc_submit_wikiwix(url, verbose) == 0) + ok++; + } + if (services & SVC_ARCHIVEPH) { + if (svc_submit_archiveph(url, verbose) == 0) + ok++; + } + + return ok; +} diff --git a/services.h b/services.h @@ -0,0 +1,25 @@ +/* See LICENSE file for copyright and license details. */ + +#ifndef SERVICES_H +#define SERVICES_H + +/* Archive service identifiers */ +#define SVC_IA (1 << 0) /* Internet Archive (Wayback Machine) */ +#define SVC_ARCHIVEPH (1 << 1) /* archive.today / archive.ph */ +#define SVC_WIKIWIX (1 << 2) /* Wikiwix */ +#define SVC_ALL (SVC_IA | SVC_ARCHIVEPH | SVC_WIKIWIX) + +/* Initialize/cleanup HTTP client */ +void svc_init(void); +void svc_cleanup(void); + +/* Submit a URL to all enabled archive services. + * Returns number of services that accepted the submission. */ +int svc_submit(const char *url, int services, int verbose); + +/* Submit to individual services (return 0 on success, -1 on failure) */ +int svc_submit_ia(const char *url, int verbose); +int svc_submit_archiveph(const char *url, int verbose); +int svc_submit_wikiwix(const char *url, int verbose); + +#endif /* SERVICES_H */ diff --git a/suploader.c b/suploader.c @@ -0,0 +1,339 @@ +/* See LICENSE file for copyright and license details. + * + * suploader - Simple Uploader + * + * Uploads URLs to web archive services (Internet Archive, + * archive.today, Wikiwix). Reads URLs from a file or stdin. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "config.h" +#include "services.h" +#include "util.h" + +/* Global options */ +static int verbose = 0; +static int daemon_mode = 0; +static int delay_secs = DEFAULT_DELAY; +static int random_order = 0; +static int services = SVC_ALL; + +static void +usage(void) +{ + fprintf(stderr, + "usage: suploader [-vrD] [-d secs] [-s services]" + " [file | -]\n" + "\n" + " -v verbose output\n" + " -r random URL selection order\n" + " -D daemon mode (loop forever)\n" + " -d secs delay between submissions" + " (default: %d)\n" + " -s services comma-separated: ia,archiveph," + "wikiwix (default: all)\n" + "\n" + " file file with URLs, one per line\n" + " - read from stdin\n", + DEFAULT_DELAY); + exit(1); +} + +/* Read all URLs from a file into an array. + * Returns array of strings, sets *count. + * Caller frees the array and each string. */ +static char ** +read_urls(const char *path, int *count) +{ + FILE *fp; + char line[MAX_URL_LEN]; + char **urls; + int n, cap; + char *trimmed; + + if (strcmp(path, "-") == 0) + fp = stdin; + else + fp = fopen(path, "r"); + + if (!fp) + die("cannot open: %s:", path); + + cap = 256; + n = 0; + urls = xmalloc(cap * sizeof(char *)); + + while (fgets(line, sizeof(line), fp)) { + /* Strip newline */ + line[strcspn(line, "\r\n")] = '\0'; + trimmed = str_trim(line); + + /* Skip empty lines and comments */ + if (!*trimmed || *trimmed == '#') + continue; + + /* Must look like a URL */ + if (!str_starts_with(trimmed, "http://") && + !str_starts_with(trimmed, "https://")) + continue; + + if (n >= cap) { + cap *= 2; + urls = xrealloc(urls, + cap * sizeof(char *)); + } + urls[n++] = xstrdup(trimmed); + } + + if (fp != stdin) + fclose(fp); + + *count = n; + return urls; +} + +/* Write remaining URLs back to the file (for daemon mode) */ +static void +write_urls(const char *path, char **urls, int count) +{ + FILE *fp; + int i; + + if (strcmp(path, "-") == 0) + return; + + fp = fopen(path, "w"); + if (!fp) { + warn("cannot write: %s", path); + return; + } + + for (i = 0; i < count; i++) { + if (urls[i]) + fprintf(fp, "%s\n", urls[i]); + } + + fclose(fp); +} + +/* Remove an entry from the URL array by setting it to NULL */ +static void +remove_url(char **urls, int idx) +{ + free(urls[idx]); + urls[idx] = NULL; +} + +/* Count non-NULL entries */ +static int +count_remaining(char **urls, int count) +{ + int i, n; + + n = 0; + for (i = 0; i < count; i++) { + if (urls[i]) + n++; + } + return n; +} + +/* Pick a random non-NULL index */ +static int +pick_random(char **urls, int count) +{ + int remaining, target, i; + + remaining = count_remaining(urls, count); + if (remaining == 0) + return -1; + + target = rand() % remaining; + for (i = 0; i < count; i++) { + if (urls[i]) { + if (target == 0) + return i; + target--; + } + } + return -1; +} + +/* Pick next non-NULL index sequentially */ +static int +pick_next(char **urls, int count) +{ + int i; + + for (i = 0; i < count; i++) { + if (urls[i]) + return i; + } + return -1; +} + +/* Parse the -s services flag */ +static int +parse_services(const char *arg) +{ + int svc; + char *copy, *tok, *saveptr; + + svc = 0; + copy = xstrdup(arg); + + for (tok = strtok_r(copy, ",", &saveptr); tok; + tok = strtok_r(NULL, ",", &saveptr)) { + tok = str_trim(tok); + if (strcmp(tok, "ia") == 0) + svc |= SVC_IA; + else if (strcmp(tok, "archiveph") == 0) + svc |= SVC_ARCHIVEPH; + else if (strcmp(tok, "wikiwix") == 0) + svc |= SVC_WIKIWIX; + else if (strcmp(tok, "all") == 0) + svc = SVC_ALL; + else + die("unknown service: %s", tok); + } + + free(copy); + return svc ? svc : SVC_ALL; +} + +int +main(int argc, char *argv[]) +{ + const char *file; + char **urls; + int count, idx, ok, processed, opt; + time_t start; + + while ((opt = getopt(argc, argv, "vrDd:s:h")) != -1) { + switch (opt) { + case 'v': + verbose = 1; + break; + case 'r': + random_order = 1; + break; + case 'D': + daemon_mode = 1; + break; + case 'd': + delay_secs = atoi(optarg); + if (delay_secs < 1) + delay_secs = 1; + break; + case 's': + services = parse_services(optarg); + break; + case 'h': /* fallthrough */ + default: + usage(); + } + } + + if (optind >= argc) + usage(); + + file = argv[optind]; + srand(time(NULL)); + + fprintf(stderr, "%s %s\n", PROG_NAME, PROG_VERSION); + if (services & SVC_IA) + fprintf(stderr, " service: Internet Archive\n"); + if (services & SVC_ARCHIVEPH) + fprintf(stderr, " service: archive.ph\n"); + if (services & SVC_WIKIWIX) + fprintf(stderr, " service: Wikiwix\n"); + fprintf(stderr, " delay: %ds\n", delay_secs); + if (daemon_mode) + fprintf(stderr, " mode: daemon\n"); + fprintf(stderr, "\n"); + + svc_init(); + + processed = 0; + start = time(NULL); + + do { + urls = read_urls(file, &count); + + if (count == 0) { + if (daemon_mode) { + if (verbose) + fprintf(stderr, + "queue empty, waiting..." + "\n"); + sleep(delay_secs); + free(urls); + continue; + } + fprintf(stderr, "no URLs to process\n"); + free(urls); + break; + } + + fprintf(stderr, "loaded %d URL(s)\n", count); + + while (count_remaining(urls, count) > 0) { + if (random_order) + idx = pick_random(urls, count); + else + idx = pick_next(urls, count); + + if (idx < 0) + break; + + fprintf(stderr, "[%d] %s\n", + processed + 1, urls[idx]); + + ok = svc_submit(urls[idx], services, + verbose); + fprintf(stderr, " %d service(s) OK\n", ok); + + remove_url(urls, idx); + processed++; + + /* Write back remaining URLs */ + if (daemon_mode) + write_urls(file, urls, count); + + /* Rate limit */ + if (count_remaining(urls, count) > 0 || + daemon_mode) + sleep(delay_secs); + + /* In non-daemon mode, process all URLs */ + if (!daemon_mode) + continue; + + /* In daemon mode, re-read file each cycle + * in case new URLs were appended */ + break; + } + + /* Free URL array */ + { + int i; + + for (i = 0; i < count; i++) + free(urls[i]); + free(urls); + } + + } while (daemon_mode); + + fprintf(stderr, + "\nDone: %d URLs processed in %lds\n", + processed, (long)(time(NULL) - start)); + + svc_cleanup(); + return 0; +} diff --git a/util.c b/util.c @@ -0,0 +1,90 @@ +/* See LICENSE file for copyright and license details. */ + +#include <ctype.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "util.h" + +void +die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + if (fmt[0] && fmt[strlen(fmt) - 1] == ':') { + fputc(' ', stderr); + perror(NULL); + } else { + fputc('\n', stderr); + } + exit(1); +} + +void +warn(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + fprintf(stderr, "warning: "); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); +} + +void * +xmalloc(size_t size) +{ + void *p = malloc(size); + + if (!p) + die("malloc:"); + return p; +} + +void * +xrealloc(void *ptr, size_t size) +{ + void *p = realloc(ptr, size); + + if (!p) + die("realloc:"); + return p; +} + +char * +xstrdup(const char *s) +{ + char *p = strdup(s); + + if (!p) + die("strdup:"); + return p; +} + +char * +str_trim(char *str) +{ + char *end; + + while (isspace((unsigned char)*str)) + str++; + if (*str == '\0') + return str; + end = str + strlen(str) - 1; + while (end > str && isspace((unsigned char)*end)) + end--; + end[1] = '\0'; + return str; +} + +int +str_starts_with(const char *str, const char *prefix) +{ + return strncmp(str, prefix, strlen(prefix)) == 0; +} diff --git a/util.h b/util.h @@ -0,0 +1,21 @@ +/* See LICENSE file for copyright and license details. */ + +#ifndef UTIL_H +#define UTIL_H + +#include <stddef.h> + +/* Memory allocation with error handling */ +void *xmalloc(size_t size); +void *xrealloc(void *ptr, size_t size); +char *xstrdup(const char *s); + +/* String utilities */ +char *str_trim(char *str); +int str_starts_with(const char *str, const char *prefix); + +/* Error handling */ +void die(const char *fmt, ...); +void warn(const char *fmt, ...); + +#endif /* UTIL_H */