/* 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 */ p = norm; if (str_starts_with(p, "https://")) p += 8; else if (str_starts_with(p, "http://")) p += 7; while (*p && *p != '/') *p++ = tolower((unsigned char)*p); /* 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 *path_start; char *path, *query, *hash, *new_path; size_t len, new_len; (void)base_domain; path_start = url; /* Skip protocol */ if (str_starts_with(url, "https://")) path_start = url + 8; else if (str_starts_with(url, "http://")) path_start = url + 7; /* Skip domain */ while (*path_start && *path_start != '/') path_start++; /* No path or just "/" -> index.html */ if (!*path_start || strcmp(path_start, "/") == 0) return xstrdup("index.html"); /* Skip leading slash */ if (*path_start == '/') path_start++; /* Copy path, strip query/fragment */ path = xstrdup(path_start); 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--; } /* If path doesn't end in .html/.htm, treat as directory */ if (len > 0 && !str_ends_with(path, ".html") && !str_ends_with(path, ".htm")) { 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; }