/* See LICENSE file for copyright and license details. * * sparser - Simple Parser * * Extracts external URLs from text files. * Supports HTML, Markdown, MDX, plain text. * Can recursively walk directories. */ #include #include #include #include #include #include #include "config.h" #include "extract.h" #include "util.h" /* Hash table for URL deduplication */ #define DEDUP_SIZE 65521 typedef struct DeNode { char *url; struct DeNode *next; } DeNode; /* Global options */ static int verbose = 0; static int recurse = 0; static int dedup = 0; static DeNode *dedup_table[DEDUP_SIZE]; static void usage(void) { fprintf(stderr, "usage: sparser [-vuR] [path | -]\n" "\n" " -v verbose (print filenames to stderr)\n" " -u deduplicate URLs\n" " -R recursive directory scan\n" "\n" " path file or directory to scan\n" " - read from stdin\n"); exit(1); } /* FNV-1a hash */ static unsigned long fnv1a(const char *s) { unsigned long h = 2166136261UL; for (; *s; s++) { h ^= (unsigned char)*s; h *= 16777619UL; } return h; } static int dedup_seen(const char *url) { unsigned long h; DeNode *n; h = fnv1a(url) % DEDUP_SIZE; for (n = dedup_table[h]; n; n = n->next) { if (strcmp(n->url, url) == 0) return 1; } return 0; } static void dedup_add(const char *url) { unsigned long h; DeNode *n; h = fnv1a(url) % DEDUP_SIZE; n = xmalloc(sizeof(DeNode)); n->url = xstrdup(url); n->next = dedup_table[h]; dedup_table[h] = n; } static void dedup_free(void) { size_t i; DeNode *n, *next; for (i = 0; i < DEDUP_SIZE; i++) { for (n = dedup_table[i]; n; n = next) { next = n->next; free(n->url); free(n); } } } /* Callback for each extracted URL */ static void url_found(const char *url, void *ctx) { (void)ctx; if (dedup) { if (dedup_seen(url)) return; dedup_add(url); } puts(url); } /* Check if a filename has a text-like extension */ static int is_text_ext(const char *name) { /* Common text extensions we want to process */ static const char *exts[] = { ".html", ".htm", ".xhtml", ".md", ".mdx", ".markdown", ".txt", ".text", ".rst", ".xml", ".rss", ".atom", ".json", ".yaml", ".yml", ".css", ".js", ".jsx", ".ts", ".tsx", ".org", ".adoc", ".tex", ".bib", ".csv", ".tsv", ".cfg", ".conf", ".ini", ".sh", ".bash", ".zsh", ".fish", ".py", ".rb", ".pl", ".c", ".h", ".go", ".rs", ".java", ".hs", NULL }; int i; for (i = 0; exts[i]; i++) { if (str_ends_with(name, exts[i])) return 1; } /* Files without extension (README, LICENSE, etc.) */ if (!strchr(name, '.')) return 1; return 0; } /* Read entire file into memory. Returns NULL on error. */ static char * read_file(const char *path, size_t *out_len) { FILE *fp; char *data; long fsize; if (strcmp(path, "-") == 0) { /* Read stdin into buffer */ size_t cap, len, n; cap = 4096; len = 0; data = xmalloc(cap); while ((n = fread(data + len, 1, cap - len, stdin)) > 0) { len += n; if (len >= cap) { cap *= 2; if (cap > MAX_FILE_SIZE) break; data = xrealloc(data, cap); } } data[len] = '\0'; *out_len = len; return data; } fp = fopen(path, "rb"); if (!fp) return NULL; if (fseek(fp, 0, SEEK_END) != 0) { fclose(fp); return NULL; } fsize = ftell(fp); if (fsize < 0 || fsize > MAX_FILE_SIZE) { fclose(fp); return NULL; } rewind(fp); data = xmalloc(fsize + 1); if (fread(data, 1, fsize, fp) != (size_t)fsize) { free(data); fclose(fp); return NULL; } data[fsize] = '\0'; fclose(fp); *out_len = fsize; return data; } /* Process a single file */ static void process_file(const char *path) { char *data; size_t len; if (verbose) fprintf(stderr, "%s\n", path); data = read_file(path, &len); if (!data) { if (verbose) warn("cannot read: %s", path); return; } if (len == 0) { free(data); return; } /* Skip binary files */ if (is_binary(data, len)) { if (verbose) fprintf(stderr, " skip binary: %s\n", path); free(data); return; } extract_urls(data, len, url_found, NULL); free(data); } /* Recursively walk a directory */ static void walk_dir(const char *dirpath) { DIR *d; struct dirent *ent; struct stat st; char path[4096]; d = opendir(dirpath); if (!d) { warn("cannot open directory: %s", dirpath); return; } while ((ent = readdir(d)) != NULL) { /* Skip hidden files and . / .. */ if (ent->d_name[0] == '.') continue; /* Skip common non-content directories */ if (strcmp(ent->d_name, "node_modules") == 0 || strcmp(ent->d_name, ".git") == 0 || strcmp(ent->d_name, "__pycache__") == 0 || strcmp(ent->d_name, "vendor") == 0 || strcmp(ent->d_name, ".next") == 0 || strcmp(ent->d_name, "dist") == 0 || strcmp(ent->d_name, "build") == 0) continue; snprintf(path, sizeof(path), "%s/%s", dirpath, ent->d_name); if (stat(path, &st) != 0) continue; if (S_ISDIR(st.st_mode)) { walk_dir(path); } else if (S_ISREG(st.st_mode)) { if (is_text_ext(ent->d_name)) process_file(path); } } closedir(d); } int main(int argc, char *argv[]) { const char *path; struct stat st; int opt; while ((opt = getopt(argc, argv, "vuRh")) != -1) { switch (opt) { case 'v': verbose = 1; break; case 'u': dedup = 1; break; case 'R': recurse = 1; break; case 'h': /* fallthrough */ default: usage(); } } if (optind >= argc) usage(); path = argv[optind]; /* Reading from stdin */ if (strcmp(path, "-") == 0) { process_file("-"); goto done; } if (stat(path, &st) != 0) die("cannot stat: %s:", path); if (S_ISDIR(st.st_mode)) { if (!recurse) die("use -R to scan directories"); walk_dir(path); } else if (S_ISREG(st.st_mode)) { process_file(path); } else { die("not a regular file or directory: %s", path); } done: if (dedup) dedup_free(); return 0; }