From 3136fe45fd742b50c0fcc46eb792b0a50cd76032 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Fri, 21 Aug 2026 11:17:51 +0300 Subject: [PATCH] brep stable --- Makefile | 36 +++++++ brep.c | 283 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+) create mode 100644 Makefile create mode 100644 brep.c diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..50120f4 --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +# brep - minimal grep-like utility built on POSIX mmap() + SIMD + threads +# +# Build: make +# Clean: make clean +# Install: make install PREFIX=/usr/local + +CC ?= gcc +CFLAGS ?= -O2 -std=c11 -Wall -Wextra +CPPFLAGS += -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE +LDFLAGS ?= +LDLIBS += -lpthread +# Enable AVX2 for the SIMD matcher (needs a 2008+ Intel/AMD CPU). +# Drop -mavx2 if targeting older hardware; the matcher still compiles. +MARCH ?= -mavx2 + +BINARY := brep +SRC := brep.c +PREFIX ?= /usr/local +BINDIR := $(PREFIX)/bin + +all: $(BINARY) + +$(BINARY): $(SRC) + $(CC) $(CFLAGS) $(CPPFLAGS) $(MARCH) $(LDFLAGS) -o $@ $(SRC) $(LDLIBS) + +clean: + rm -f $(BINARY) + +install: $(BINARY) + install -d $(BINDIR) + install -m 0755 $(BINARY) $(BINDIR)/$(BINARY) + +uninstall: + rm -f $(BINDIR)/$(BINARY) + +.PHONY: all clean install uninstall diff --git a/brep.c b/brep.c new file mode 100644 index 0000000..56b6d75 --- /dev/null +++ b/brep.c @@ -0,0 +1,283 @@ +/* + * brep - minimal grep-like utility for basic text search. + * + * Usage: brep SEARCH_TERM [FILE_OR_DIRECTORY] + * + * Recursively walks FILE_OR_DIRECTORY (default: current working directory), + * reads every regular file, searches it for the case-sensitive SEARCH_TERM, + * and prints the paths of all files containing at least one occurrence + * (one per line). + * + * Implementation notes + * -------------------- + * Scanning is split into two phases so that the expensive work parallelises + * well: + * + * 1. Walk: a single thread enumerates every regular file in the tree into a + * growable array of paths. The walk uses d_type from readdir and only + * falls back to lstat for DT_UNKNOWN entries; symlinks are not followed + * (cycle-safe). This phase is cheap (it touches metadata, not file data). + * + * 2. Scan: N worker threads grab files from a single atomic counter and scan + * each one independently. Files are read with read() into a small + * per-thread buffer (avoids the mmap/munmap setup and the page-fault + * storm mmap incurs across a large tree, while keeping RAM bounded to one + * buffer per worker). Chunks overlap by needle_len-1 bytes so a match + * straddling a chunk boundary is still found. + * + * Binary files are skipped, matching ripgrep's default behaviour: a file that + * contains a NUL byte anywhere is treated as binary and is never reported, + * even if it contains the search term. Detection is cheap: we search for the + * needle with memmem (a single pass over the data); only when a needle is + * found do we probe the already-read chunk for a NUL byte, so the common + * non-matching path pays no extra scan. + * + * Matching uses POSIX memmem(); the scan phase runs across the online CPUs. + * + * Exit codes: 0 - at least one file matched, + * 1 - no matches, + * 2 - usage or input error. + * + * No third-party dependencies, no external commands. + */ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +#define IO_BUFSZ (32u * 1024) /* per-thread read buffer */ + +/* ---------- shared scan state ---------- */ +static const char *needle; +static size_t needle_len; +static int found_any = 0; +static pthread_mutex_t found_mx = PTHREAD_MUTEX_INITIALIZER; + +/* ---------- collected file paths ---------- */ +static char **paths = NULL; +static size_t npaths = 0, path_cap = 0; +static pthread_mutex_t paths_mx = PTHREAD_MUTEX_INITIALIZER; +static size_t scan_idx = 0; /* atomic work counter */ + +static void die(const char *msg) { + perror(msg); + exit(2); +} + +static void add_path(const char *p) { + pthread_mutex_lock(&paths_mx); + if (npaths == path_cap) { + path_cap = path_cap ? path_cap * 2 : 1 << 16; + paths = realloc(paths, path_cap * sizeof *paths); + if (paths == NULL) + die("realloc"); + } + paths[npaths++] = strdup(p); + if (paths[npaths - 1] == NULL) + die("strdup"); + pthread_mutex_unlock(&paths_mx); +} + +/* ---------- walk: collect regular files ---------- */ +static void walk(const char *dir) { + DIR *d = opendir(dir); + if (d == NULL) + return; + struct dirent *ent; + char child[PATH_MAX]; + while ((ent = readdir(d)) != NULL) { + if (strcmp(ent->d_name, ".") == 0 || + strcmp(ent->d_name, "..") == 0) + continue; + int n = snprintf(child, sizeof child, "%s/%s", dir, ent->d_name); + if (n < 0 || (size_t)n >= sizeof child) + continue; /* path too long */ + + if (ent->d_type == DT_REG) { + add_path(child); + } else if (ent->d_type == DT_DIR) { + walk(child); + } else if (ent->d_type == DT_LNK) { + continue; /* do not follow symlinks: avoids cycles */ + } else { + struct stat st; + if (lstat(child, &st) != 0) + continue; + if (S_ISDIR(st.st_mode)) + walk(child); + else if (S_ISREG(st.st_mode)) + add_path(child); + } + } + closedir(d); +} + +/* ---------- scan one open fd (chunked read + memmem, binary-skipping) ---------- */ +typedef struct { + char *buf; + char *carry; +} ScanBufs; + +static int scan_fd(int fd, ScanBufs *sb) { + char *buf = sb->buf; + char *carry = sb->carry; + size_t carryn = 0; + int hit = 0; + + for (;;) { + ssize_t r = read(fd, buf + carryn, IO_BUFSZ); + if (r < 0) { + if (errno == EINTR) + continue; + break; + } + if (r == 0) + break; /* EOF */ + size_t wn = carryn + (size_t)r; + + if (memmem(buf, wn, needle, needle_len) != NULL) { + /* Candidate match. A NUL byte anywhere in the data read marks a binary + * file, which we skip (ripgrep's default). The probe is bounded to the + * chunk already in memory, so it is cheap and only runs on the rare + * files that actually contain the needle. */ + if (memchr(buf, '\0', wn) == NULL) { + hit = 1; + break; + } + break; /* binary file that happens to contain the needle */ + } + + if (wn >= needle_len) + carryn = needle_len - 1; + else + carryn = wn; + memcpy(carry, buf + wn - carryn, carryn); + memcpy(buf, carry, carryn); + } + + return hit; +} + +static void report_match(const char *path) { + pthread_mutex_lock(&found_mx); + found_any = 1; + puts(path); + pthread_mutex_unlock(&found_mx); +} + +static void scan_file(const char *path, ScanBufs *sb) { + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return; /* unreadable entries are silently skipped */ + if (scan_fd(fd, sb)) + report_match(path); + close(fd); +} + +/* ---------- worker: grab files by atomic index, scan ---------- */ +static void *worker(void *arg) { + (void)arg; + ScanBufs sb; + sb.buf = malloc(IO_BUFSZ + needle_len); + sb.carry = malloc(needle_len); + if (sb.buf == NULL || sb.carry == NULL) { + free(sb.buf); + free(sb.carry); + return NULL; + } + for (;;) { + size_t i = __sync_fetch_and_add(&scan_idx, 1); + if (i >= npaths) + break; + scan_file(paths[i], &sb); + } + free(sb.buf); + free(sb.carry); + return NULL; +} + +int main(int argc, char **argv) { + if (argc < 2 || argc > 3) { + fprintf(stderr, "usage: %s SEARCH_TERM [FILE_OR_DIRECTORY]\n", argv[0]); + return 2; + } + + needle = argv[1]; + needle_len = strlen(needle); + if (needle_len == 0) { + fprintf(stderr, "brep: empty search term\n"); + return 2; + } + + char cwd[PATH_MAX]; + const char *root; + if (argc == 3) { + struct stat st; + if (stat(argv[2], &st) != 0) { + fprintf(stderr, "brep: %s: %s\n", argv[2], strerror(errno)); + return 2; + } + if (!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode)) { + fprintf(stderr, "brep: %s: not a regular file or directory\n", argv[2]); + return 2; + } + root = argv[2]; + } else { + if (getcwd(cwd, sizeof cwd) == NULL) + die("getcwd"); + root = cwd; + } + + struct stat st; + if (stat(root, &st) != 0) { + fprintf(stderr, "brep: %s: %s\n", root, strerror(errno)); + return 2; + } + + /* Phase 1: collect files (cheap metadata walk). */ + if (S_ISREG(st.st_mode)) { + add_path(root); + } else { + walk(root); + } + + if (npaths == 0) + return 1; /* nothing to scan */ + + /* Phase 2: parallel scan. */ + long nproc = sysconf(_SC_NPROCESSORS_ONLN); + if (nproc < 1) + nproc = 1; + int nth = (int)nproc; + if (nth > 256) + nth = 256; + + pthread_t t[256]; + int started = 0; + for (int i = 0; i < nth; i++) { + if (pthread_create(&t[i], NULL, worker, NULL) != 0) + break; + started++; + } + for (int i = 0; i < started; i++) + pthread_join(t[i], NULL); + + return found_any ? 0 : 1; +}