/* sensor.c — see sensor.h. Pure POSIX/libc. */
#define _POSIX_C_SOURCE 200809L /* strdup, lstat under -std=c11 */
#include "sensor.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
/* ---- feature interning ---- */
static int intern(FeatureTable *t, const char *s) {
for (size_t i = 0; i < t->count; i++)
if (strcmp(t->names[i], s) == 0) return (int)i;
if (t->count == t->cap) {
t->cap = t->cap ? t->cap * 2 : 32;
t->names = realloc(t->names, t->cap * sizeof *t->names);
}
t->names[t->count] = strdup(s);
return (int)t->count++;
}
const char *ext_name(const Corpus *c, int f) {
return (f < 0 || (size_t)f >= c->ext.count) ? "?" : c->ext.names[f];
}
const char *dir_name(const Corpus *c, int f) {
return (f < 0 || (size_t)f >= c->dir.count) ? "?" : c->dir.names[f];
}
/* lowercase extension after last '.' in basename; "<none>" if absent. */
static void ext_of(const char *path, char *out, size_t n) {
const char *base = strrchr(path, '/');
base = base ? base + 1 : path;
const char *dot = strrchr(base, '.');
if (!dot || dot == base) { snprintf(out, n, "<none>"); return; }
size_t k = 0;
for (const char *p = dot + 1; *p && k + 1 < n; p++) {
char ch = *p;
out[k++] = (ch >= 'A' && ch <= 'Z') ? ch + 32 : ch;
}
out[k] = '\0';
}
/* basename of the parent directory. */
static void dir_of(const char *path, char *out, size_t n) {
const char *slash = strrchr(path, '/');
if (!slash || slash == path) { snprintf(out, n, "<root>"); return; }
const char *p = slash - 1;
while (p > path && *p != '/') p--;
if (*p == '/') p++;
size_t k = 0;
for (const char *q = p; q < slash && k + 1 < n; q++) out[k++] = *q;
out[k] = '\0';
}
/* ---- corpus collection ---- */
static void push(Corpus *c, const char *path, int fe, int fd) {
if (c->count == c->cap) {
c->cap = c->cap ? c->cap * 2 : 256;
c->files = realloc(c->files, c->cap * sizeof *c->files);
}
c->files[c->count++] = (File){ .path = strdup(path), .f_ext = fe, .f_dir = fd, .klass = -1 };
}
static void walk(Corpus *c, const char *dir) {
DIR *d = opendir(dir);
if (!d) return;
struct dirent *e;
char path[4096], ext[64], dname[256];
while ((e = readdir(d))) {
if (e->d_name[0] == '.') continue;
snprintf(path, sizeof path, "%s/%s", dir, e->d_name);
struct stat st;
if (lstat(path, &st) != 0) continue;
if (S_ISDIR(st.st_mode)) {
walk(c, path);
} else if (S_ISREG(st.st_mode)) {
ext_of(path, ext, sizeof ext);
dir_of(path, dname, sizeof dname);
push(c, path, intern(&c->ext, ext), intern(&c->dir, dname));
}
}
closedir(d);
}
int corpus_collect(Corpus *c, const char *root) {
memset(c, 0, sizeof *c);
walk(c, root);
return c->count > 0 ? 0 : -1;
}
void corpus_free(Corpus *c) {
for (size_t i = 0; i < c->count; i++) free(c->files[i].path);
for (size_t i = 0; i < c->ext.count; i++) free(c->ext.names[i]);
for (size_t i = 0; i < c->dir.count; i++) free(c->dir.names[i]);
free(c->files); free(c->ext.names); free(c->dir.names);
memset(c, 0, sizeof *c);
}
/* ---- the observation: is any ancestor dir a build/artifact marker? ---- */
static const char *MARKERS[] = {
"/node_modules/", "/dist/", "/build/", "/__pycache__/", "/.git/",
"/target/", "/vendor/", "/.cache/", "/out/", "/obj/", "/coverage/",
"/.next/", "/.tox/", "/.venv/", "/generated/", "/bin/", 0
};
static int path_is_generated(const char *path) {
for (int i = 0; MARKERS[i]; i++)
if (strstr(path, MARKERS[i])) return 1;
return 0;
}
int sensor_open(Corpus *c, size_t i) {
File *f = &c->files[i];
if (f->klass >= 0) return f->klass;
f->klass = path_is_generated(f->path) ? CLASS_GENERATED : CLASS_SOURCE;
return f->klass;
}