/* See LICENSE file for copyright and license details. */ #include #include #include #include #include "crawl.h" #include "util.h" /* FNV-1a hash - fast, good distribution for URL strings */ static unsigned long fnv1a(const char *s) { unsigned long h = 2166136261UL; for (; *s; s++) { h ^= (unsigned char)*s; h *= 16777619UL; } return h; } UrlQueue * queue_new(void) { UrlQueue *q = xmalloc(sizeof(UrlQueue)); q->head = NULL; q->tail = NULL; q->count = 0; return q; } void queue_free(UrlQueue *q) { QueueNode *n, *next; if (!q) return; for (n = q->head; n; n = next) { next = n->next; free(n->url); free(n); } free(q); } void queue_push(UrlQueue *q, const char *url, int depth) { QueueNode *n = xmalloc(sizeof(QueueNode)); n->url = xstrdup(url); n->depth = depth; n->next = NULL; if (q->tail) { q->tail->next = n; q->tail = n; } else { q->head = n; q->tail = n; } q->count++; } QueueNode * queue_pop(UrlQueue *q) { QueueNode *n; if (!q->head) return NULL; n = q->head; q->head = n->next; if (!q->head) q->tail = NULL; q->count--; return n; } int queue_empty(UrlQueue *q) { return q->head == NULL; } size_t queue_size(UrlQueue *q) { return q->count; } VisitedSet * visited_new(void) { VisitedSet *v = xmalloc(sizeof(VisitedSet)); memset(v->buckets, 0, sizeof(v->buckets)); v->count = 0; return v; } void visited_free(VisitedSet *v) { HashNode *n, *next; size_t i; if (!v) return; for (i = 0; i < HT_SIZE; i++) { for (n = v->buckets[i]; n; n = next) { next = n->next; free(n->url); free(n); } } free(v); } void visited_add(VisitedSet *v, const char *url) { unsigned long h = fnv1a(url) % HT_SIZE; HashNode *n; /* Check for duplicate first */ for (n = v->buckets[h]; n; n = n->next) { if (strcmp(n->url, url) == 0) return; } n = xmalloc(sizeof(HashNode)); n->url = xstrdup(url); n->next = v->buckets[h]; v->buckets[h] = n; v->count++; } int visited_contains(VisitedSet *v, const char *url) { unsigned long h = fnv1a(url) % HT_SIZE; HashNode *n; for (n = v->buckets[h]; n; n = n->next) { if (strcmp(n->url, url) == 0) return 1; } return 0; } size_t visited_count(VisitedSet *v) { return v->count; } char * url_normalize(const char *url) { char *norm, *p, *hash, *query; size_t len; norm = xstrdup(url); /* Remove fragment */ hash = strchr(norm, '#'); if (hash) *hash = '\0'; /* Remove query string */ query = strchr(norm, '?'); if (query) *query = '\0'; /* Remove trailing slash (but not bare domain slash) */ len = strlen(norm); if (len > 1 && norm[len - 1] == '/') { /* Keep slash if it's just protocol://domain/ */ p = norm; if (str_starts_with(p, "https://")) p += 8; else if (str_starts_with(p, "http://")) p += 7; /* Skip domain */ while (*p && *p != '/') p++; /* Only strip if there's path beyond domain */ if (p < norm + len - 1) norm[len - 1] = '\0'; } /* Lowercase the domain part (use url_path to find where domain ends) */ { const char *path = url_path(norm); char *dom_end = (char *)path; for (char *q = norm; q < dom_end; q++) *q = tolower((unsigned char)*q); } /* Remove default port :80 or :443 */ p = norm; if (str_starts_with(p, "https://")) p += 8; else if (str_starts_with(p, "http://")) p += 7; { char *colon = NULL; char *slash = NULL; char *scan; int is_https; is_https = str_starts_with(norm, "https://"); for (scan = p; *scan && *scan != '/'; scan++) { if (*scan == ':') colon = scan; } slash = scan; if (colon) { char port[8]; size_t plen = slash - colon - 1; if (plen < sizeof(port)) { memcpy(port, colon + 1, plen); port[plen] = '\0'; if ((is_https && strcmp(port, "443") == 0) || (!is_https && strcmp(port, "80") == 0)) { memmove(colon, slash, strlen(slash) + 1); } } } } return norm; } char * url_to_path(const char *url, const char *base_domain) { const char *p; char *path, *query, *hash, *new_path; size_t len, new_len; (void)base_domain; /* Use the new central helper — removes 8 lines of duplication */ p = url_path(url); /* No path or just "/" -> index.html */ if (!*p || strcmp(p, "/") == 0) return xstrdup("index.html"); /* Skip the leading slash for the stored path */ p++; if (*p == '\0') return xstrdup("index.html"); /* Copy path, strip query/fragment */ path = xstrdup(p); query = strchr(path, '?'); if (query) *query = '\0'; hash = strchr(path, '#'); if (hash) *hash = '\0'; /* Remove trailing slash */ len = strlen(path); if (len > 0 && path[len - 1] == '/') { path[len - 1] = '\0'; len--; } /* Basic sanitization for messy legacy filenames (vintage dumps, etc.) */ { char *s; for (s = path; *s; s++) { /* Replace dangerous or annoying characters */ if (*s == '\\' || *s == ':' || *s == '*' || *s == '?' || *s == '"' || *s == '<' || *s == '>' || *s == '|') { *s = '_'; } } } /* If last path segment has no extension (no '.'), treat as directory */ { const char *last = strrchr(path, '/'); last = last ? last + 1 : path; if (len > 0 && strchr(last, '.') == NULL) { new_len = len + 12; new_path = xmalloc(new_len); snprintf(new_path, new_len, "%s/index.html", path); free(path); path = new_path; } } return path; }