/* * 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; }