File Coverage

include/eshu_file.h
Criterion Covered Total %
statement 167 255 65.4
branch 130 348 37.3
condition n/a
subroutine n/a
pod n/a
total 297 603 49.2


line stmt bran cond sub pod time code
1             /*
2             * eshu_file.h — File and directory level indentation operations
3             *
4             * Pure C (POSIX), no Perl dependencies.
5             * Requires: eshu.h, eshu_c.h, eshu_pl.h, eshu_xs.h, eshu_xml.h,
6             * eshu_css.h, eshu_diff.h
7             */
8              
9             #ifndef ESHU_FILE_H
10             #define ESHU_FILE_H
11              
12             #include "eshu.h"
13             #include "eshu_diff.h"
14             #include
15             #include
16             #include
17             #ifndef _WIN32
18             # include
19             # include
20             #else
21             # include
22             # include
23             # ifdef __MINGW32__
24             # include
25             # else
26             # include "dirent_win.h" /* MSVC needs a compat header */
27             # endif
28             # ifndef S_ISREG
29             # define S_ISREG(m) (((m) & _S_IFMT) == _S_IFREG)
30             # endif
31             # ifndef S_ISDIR
32             # define S_ISDIR(m) (((m) & _S_IFMT) == _S_IFDIR)
33             # endif
34             # ifndef S_ISLNK
35             # define S_ISLNK(m) 0 /* Windows has no symlinks via stat */
36             # endif
37             # define lstat stat
38             #endif
39              
40             #define ESHU_MAX_FILE_SIZE 1048576 /* 1MB */
41             #define ESHU_BINARY_SAMPLE 8192
42              
43             /* ══════════════════════════════════════════════════════════════════
44             * Status codes for indent_file results
45             * ══════════════════════════════════════════════════════════════════ */
46              
47             enum eshu_file_status {
48             ESHU_STATUS_UNCHANGED = 0,
49             ESHU_STATUS_CHANGED = 1,
50             ESHU_STATUS_NEEDS_FIXING = 2,
51             ESHU_STATUS_SKIPPED = 3,
52             ESHU_STATUS_ERROR = 4
53             };
54              
55             /* ══════════════════════════════════════════════════════════════════
56             * File result
57             * ══════════════════════════════════════════════════════════════════ */
58              
59             typedef struct {
60             char *file; /* path (strdup'd, caller frees) */
61             int status; /* eshu_file_status */
62             char *lang; /* language string or NULL */
63             char *reason; /* skip reason or NULL */
64             char *error; /* error message or NULL */
65             char *diff; /* diff string or NULL */
66             size_t diff_len;
67             } eshu_file_result_t;
68              
69 39           static void eshu_file_result_init(eshu_file_result_t *r) {
70 39           memset(r, 0, sizeof(*r));
71 39           }
72              
73 39           static void eshu_file_result_free(eshu_file_result_t *r) {
74 39 50         if (r->file) free(r->file);
75 39 100         if (r->lang) free(r->lang);
76 39 100         if (r->reason) free(r->reason);
77 39 50         if (r->error) free(r->error);
78 39 100         if (r->diff) free(r->diff);
79 39           memset(r, 0, sizeof(*r));
80 39           }
81              
82             /* ══════════════════════════════════════════════════════════════════
83             * Directory report
84             * ══════════════════════════════════════════════════════════════════ */
85              
86             typedef struct {
87             int files_checked;
88             int files_changed;
89             int files_skipped;
90             int files_errored;
91             eshu_file_result_t *changes;
92             size_t changes_count;
93             size_t changes_cap;
94             } eshu_dir_report_t;
95              
96 16           static void eshu_dir_report_init(eshu_dir_report_t *r) {
97 16           memset(r, 0, sizeof(*r));
98 16           r->changes_cap = 64;
99 16           r->changes = (eshu_file_result_t *)malloc(r->changes_cap * sizeof(eshu_file_result_t));
100 16           }
101              
102 0           static void eshu_dir_report_push(eshu_dir_report_t *r, eshu_file_result_t *res) {
103 0 0         if (r->changes_count >= r->changes_cap) {
104 0           r->changes_cap *= 2;
105 0           r->changes = (eshu_file_result_t *)realloc(r->changes,
106 0           r->changes_cap * sizeof(eshu_file_result_t));
107             }
108 0           r->changes[r->changes_count++] = *res;
109             /* Transfer ownership — caller should not free res fields */
110 0           }
111              
112 0           static void eshu_dir_report_free(eshu_dir_report_t *r) {
113             size_t i;
114 0 0         for (i = 0; i < r->changes_count; i++)
115 0           eshu_file_result_free(&r->changes[i]);
116 0           free(r->changes);
117 0           memset(r, 0, sizeof(*r));
118 0           }
119              
120             /* ══════════════════════════════════════════════════════════════════
121             * Language detection from extension (pure C)
122             * ══════════════════════════════════════════════════════════════════ */
123              
124             /* Case-insensitive single-char compare */
125 156           static int eshu_ci(char a, char b) {
126 156 50         if (a >= 'A' && a <= 'Z') a += 32;
    50          
127 156 50         if (b >= 'A' && b <= 'Z') b += 32;
    50          
128 156           return a == b;
129             }
130              
131             /* Returns a static string: "c","xs","perl","xml","xhtml","html","css" or NULL */
132 38           static const char *eshu_detect_lang_ext(const char *path) {
133 38           const char *dot = NULL;
134 38           const char *p = path + strlen(path);
135             size_t ext_len;
136              
137 115 50         while (p > path) {
138 115           p--;
139 115 100         if (*p == '.') { dot = p + 1; break; }
140 77 50         if (*p == '/' || *p == '\\') break;
    50          
141             }
142 38 50         if (!dot) return NULL;
143 38           ext_len = strlen(dot);
144              
145             /* c, h */
146 38 100         if (ext_len == 1 && (eshu_ci(dot[0], 'c') || eshu_ci(dot[0], 'h')))
    50          
    0          
147 3           return "c";
148             /* xs */
149 35 100         if (ext_len == 2 && eshu_ci(dot[0], 'x') && eshu_ci(dot[1], 's'))
    50          
    0          
150 0           return "xs";
151             /* pl, pm */
152 35 100         if (ext_len == 2 && eshu_ci(dot[0], 'p')
    50          
153 31 100         && (eshu_ci(dot[1], 'l') || eshu_ci(dot[1], 'm')))
    50          
154 31           return "perl";
155             /* t */
156 4 50         if (ext_len == 1 && eshu_ci(dot[0], 't'))
    0          
157 0           return "perl";
158             /* xml */
159 4 50         if (ext_len == 3 && eshu_ci(dot[0], 'x') && eshu_ci(dot[1], 'm') && eshu_ci(dot[2], 'l'))
    100          
    50          
    50          
160 1           return "xml";
161             /* xsl */
162 3 50         if (ext_len == 3 && eshu_ci(dot[0], 'x') && eshu_ci(dot[1], 's') && eshu_ci(dot[2], 'l'))
    50          
    0          
    0          
163 0           return "xml";
164             /* xslt */
165 3 50         if (ext_len == 4 && eshu_ci(dot[0], 'x') && eshu_ci(dot[1], 's')
    0          
    0          
166 0 0         && eshu_ci(dot[2], 'l') && eshu_ci(dot[3], 't'))
    0          
167 0           return "xml";
168             /* svg */
169 3 50         if (ext_len == 3 && eshu_ci(dot[0], 's') && eshu_ci(dot[1], 'v') && eshu_ci(dot[2], 'g'))
    50          
    0          
    0          
170 0           return "xml";
171             /* xhtml */
172 3 50         if (ext_len == 5 && eshu_ci(dot[0], 'x') && eshu_ci(dot[1], 'h')
    0          
    0          
173 0 0         && eshu_ci(dot[2], 't') && eshu_ci(dot[3], 'm') && eshu_ci(dot[4], 'l'))
    0          
    0          
174 0           return "xhtml";
175             /* html */
176 3 50         if (ext_len == 4 && eshu_ci(dot[0], 'h') && eshu_ci(dot[1], 't')
    0          
    0          
177 0 0         && eshu_ci(dot[2], 'm') && eshu_ci(dot[3], 'l'))
    0          
178 0           return "html";
179             /* htm */
180 3 50         if (ext_len == 3 && eshu_ci(dot[0], 'h') && eshu_ci(dot[1], 't') && eshu_ci(dot[2], 'm'))
    50          
    0          
    0          
181 0           return "html";
182             /* tmpl */
183 3 50         if (ext_len == 4 && eshu_ci(dot[0], 't') && eshu_ci(dot[1], 'm')
    0          
    0          
184 0 0         && eshu_ci(dot[2], 'p') && eshu_ci(dot[3], 'l'))
    0          
185 0           return "html";
186             /* tt */
187 3 50         if (ext_len == 2 && eshu_ci(dot[0], 't') && eshu_ci(dot[1], 't'))
    0          
    0          
188 0           return "html";
189             /* ep */
190 3 50         if (ext_len == 2 && eshu_ci(dot[0], 'e') && eshu_ci(dot[1], 'p'))
    0          
    0          
191 0           return "html";
192             /* css */
193 3 50         if (ext_len == 3 && eshu_ci(dot[0], 'c') && eshu_ci(dot[1], 's') && eshu_ci(dot[2], 's'))
    100          
    50          
    50          
194 1           return "css";
195             /* scss */
196 2 50         if (ext_len == 4 && eshu_ci(dot[0], 's') && eshu_ci(dot[1], 'c')
    0          
    0          
197 0 0         && eshu_ci(dot[2], 's') && eshu_ci(dot[3], 's'))
    0          
198 0           return "css";
199             /* less */
200 2 50         if (ext_len == 4 && eshu_ci(dot[0], 'l') && eshu_ci(dot[1], 'e')
    0          
    0          
201 0 0         && eshu_ci(dot[2], 's') && eshu_ci(dot[3], 's'))
    0          
202 0           return "css";
203              
204             /* js */
205 2 50         if (ext_len == 2 && eshu_ci(dot[0], 'j') && eshu_ci(dot[1], 's'))
    0          
    0          
206 0           return "js";
207             /* jsx */
208 2 50         if (ext_len == 3 && eshu_ci(dot[0], 'j') && eshu_ci(dot[1], 's') && eshu_ci(dot[2], 'x'))
    50          
    0          
    0          
209 0           return "js";
210             /* mjs */
211 2 50         if (ext_len == 3 && eshu_ci(dot[0], 'm') && eshu_ci(dot[1], 'j') && eshu_ci(dot[2], 's'))
    50          
    0          
    0          
212 0           return "js";
213             /* cjs */
214 2 50         if (ext_len == 3 && eshu_ci(dot[0], 'c') && eshu_ci(dot[1], 'j') && eshu_ci(dot[2], 's'))
    50          
    0          
    0          
215 0           return "js";
216             /* ts */
217 2 50         if (ext_len == 2 && eshu_ci(dot[0], 't') && eshu_ci(dot[1], 's'))
    0          
    0          
218 0           return "js";
219             /* tsx */
220 2 50         if (ext_len == 3 && eshu_ci(dot[0], 't') && eshu_ci(dot[1], 's') && eshu_ci(dot[2], 'x'))
    50          
    50          
    0          
221 0           return "js";
222             /* mts */
223 2 50         if (ext_len == 3 && eshu_ci(dot[0], 'm') && eshu_ci(dot[1], 't') && eshu_ci(dot[2], 's'))
    50          
    0          
    0          
224 0           return "js";
225              
226             /* pod */
227 2 50         if (ext_len == 3 && eshu_ci(dot[0], 'p') && eshu_ci(dot[1], 'o') && eshu_ci(dot[2], 'd'))
    50          
    0          
    0          
228 0           return "pod";
229              
230 2           return NULL;
231             }
232              
233             /* ══════════════════════════════════════════════════════════════════
234             * Config from lang string
235             * ══════════════════════════════════════════════════════════════════ */
236              
237             /* Forward declarations for engine functions — headers must already be included */
238              
239 36           static int eshu_lang_from_string(const char *lang) {
240 36 50         if (!lang) return -1;
241 36 100         if (strcmp(lang, "c") == 0) return ESHU_LANG_C;
242 33 100         if (strcmp(lang, "perl") == 0) return ESHU_LANG_PERL;
243 2 50         if (strcmp(lang, "pl") == 0) return ESHU_LANG_PERL;
244 2 50         if (strcmp(lang, "xs") == 0) return ESHU_LANG_XS;
245 2 100         if (strcmp(lang, "xml") == 0) return ESHU_LANG_XML;
246 1 50         if (strcmp(lang, "xsl") == 0) return ESHU_LANG_XML;
247 1 50         if (strcmp(lang, "xslt") == 0) return ESHU_LANG_XML;
248 1 50         if (strcmp(lang, "svg") == 0) return ESHU_LANG_XML;
249 1 50         if (strcmp(lang, "xhtml") == 0) return ESHU_LANG_XML;
250 1 50         if (strcmp(lang, "html") == 0) return ESHU_LANG_HTML;
251 1 50         if (strcmp(lang, "htm") == 0) return ESHU_LANG_HTML;
252 1 50         if (strcmp(lang, "tmpl") == 0) return ESHU_LANG_HTML;
253 1 50         if (strcmp(lang, "tt") == 0) return ESHU_LANG_HTML;
254 1 50         if (strcmp(lang, "ep") == 0) return ESHU_LANG_HTML;
255 1 50         if (strcmp(lang, "css") == 0) return ESHU_LANG_CSS;
256 0 0         if (strcmp(lang, "scss") == 0) return ESHU_LANG_CSS;
257 0 0         if (strcmp(lang, "less") == 0) return ESHU_LANG_CSS;
258 0 0         if (strcmp(lang, "js") == 0) return ESHU_LANG_JS;
259 0 0         if (strcmp(lang, "javascript") == 0) return ESHU_LANG_JS;
260 0 0         if (strcmp(lang, "jsx") == 0) return ESHU_LANG_JS;
261 0 0         if (strcmp(lang, "mjs") == 0) return ESHU_LANG_JS;
262 0 0         if (strcmp(lang, "cjs") == 0) return ESHU_LANG_JS;
263 0 0         if (strcmp(lang, "ts") == 0) return ESHU_LANG_JS;
264 0 0         if (strcmp(lang, "typescript") == 0) return ESHU_LANG_JS;
265 0 0         if (strcmp(lang, "tsx") == 0) return ESHU_LANG_JS;
266 0 0         if (strcmp(lang, "mts") == 0) return ESHU_LANG_JS;
267 0 0         if (strcmp(lang, "pod") == 0) return ESHU_LANG_POD;
268 0           return -1;
269             }
270              
271             /* Dispatch to the correct indentation engine.
272             * Returns malloc'd result; caller must free. */
273 36           static char *eshu_indent_dispatch(const char *src, size_t src_len,
274             eshu_config_t *cfg, size_t *out_len)
275             {
276 36           switch (cfg->lang) {
277 31           case ESHU_LANG_PERL:
278 31           return eshu_indent_pl(src, src_len, cfg, out_len);
279 0           case ESHU_LANG_XS:
280 0           return eshu_indent_xs(src, src_len, cfg, out_len);
281 1           case ESHU_LANG_XML:
282             case ESHU_LANG_HTML:
283 1           return eshu_indent_xml(src, src_len, cfg, out_len);
284 1           case ESHU_LANG_CSS:
285 1           return eshu_indent_css(src, src_len, cfg, out_len);
286 0           case ESHU_LANG_JS:
287 0           return eshu_indent_js(src, src_len, cfg, out_len);
288 0           case ESHU_LANG_POD:
289 0           return eshu_indent_pod(src, src_len, cfg, out_len);
290 3           default:
291 3           return eshu_indent_c(src, src_len, cfg, out_len);
292             }
293             }
294              
295             /* ══════════════════════════════════════════════════════════════════
296             * indent_file — process a single file
297             * ══════════════════════════════════════════════════════════════════ */
298              
299             /* opts bitflags */
300             #define ESHU_OPT_FIX 1
301             #define ESHU_OPT_DIFF 2
302              
303 39           static void eshu_indent_file(const char *path, const eshu_config_t *cfg,
304             const char *force_lang, int opts,
305             eshu_file_result_t *result)
306             {
307             struct stat st;
308             FILE *fp;
309             char *src;
310             size_t src_len, out_len;
311             char *fixed;
312             const char *lang_str;
313             int lang_id;
314             eshu_config_t file_cfg;
315              
316 39           eshu_file_result_init(result);
317 39           result->file = strdup(path);
318              
319             /* Check file exists and is regular */
320 39 50         if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
    50          
321 0           result->status = ESHU_STATUS_ERROR;
322 0           result->error = strdup("not a readable file");
323 3           return;
324             }
325              
326             /* Size limit */
327 39 50         if (st.st_size > ESHU_MAX_FILE_SIZE) {
328 0           result->status = ESHU_STATUS_SKIPPED;
329 0           result->reason = strdup("file too large");
330 0           return;
331             }
332              
333             /* Read file */
334 39           fp = fopen(path, "rb");
335 39 50         if (!fp) {
336 0           result->status = ESHU_STATUS_ERROR;
337 0           result->error = strdup("cannot open file");
338 0           return;
339             }
340 39           src_len = (size_t)st.st_size;
341 39           src = (char *)malloc(src_len + 1);
342 39 50         if (fread(src, 1, src_len, fp) != src_len) {
343 0           fclose(fp);
344 0           free(src);
345 0           result->status = ESHU_STATUS_ERROR;
346 0           result->error = strdup("read error");
347 0           return;
348             }
349 39           fclose(fp);
350 39           src[src_len] = '\0';
351              
352             /* Binary detection: NUL in first 8KB */
353 39 50         if (src_len > 0) {
354 39           size_t check = src_len < ESHU_BINARY_SAMPLE ? src_len : ESHU_BINARY_SAMPLE;
355 39 100         if (memchr(src, '\0', check) != NULL) {
356 1           free(src);
357 1           result->status = ESHU_STATUS_SKIPPED;
358 1           result->reason = strdup("binary file");
359 1           return;
360             }
361             }
362              
363             /* Detect language */
364 38 50         lang_str = force_lang ? force_lang : eshu_detect_lang_ext(path);
365 38 100         if (!lang_str) {
366 2           free(src);
367 2           result->status = ESHU_STATUS_SKIPPED;
368 2           result->reason = strdup("unrecognised extension");
369 2           return;
370             }
371 36           lang_id = eshu_lang_from_string(lang_str);
372 36 50         if (lang_id < 0) {
373 0           free(src);
374 0           result->status = ESHU_STATUS_SKIPPED;
375 0           result->reason = strdup("unrecognised extension");
376 0           return;
377             }
378 36           result->lang = strdup(lang_str);
379              
380             /* Build config */
381 36           file_cfg = *cfg;
382 36           file_cfg.lang = lang_id;
383              
384             /* Indent */
385 36           fixed = eshu_indent_dispatch(src, src_len, &file_cfg, &out_len);
386              
387 36 100         if (out_len == src_len && memcmp(fixed, src, src_len) == 0) {
    50          
388 2           result->status = ESHU_STATUS_UNCHANGED;
389             } else {
390 68           result->status = (opts & ESHU_OPT_FIX)
391 34 100         ? ESHU_STATUS_CHANGED : ESHU_STATUS_NEEDS_FIXING;
392              
393 34 100         if (opts & ESHU_OPT_FIX) {
394 12           fp = fopen(path, "wb");
395 12 50         if (!fp) {
396 0           result->status = ESHU_STATUS_ERROR;
397 0           result->error = strdup("cannot write file");
398 0           free(src);
399 0           free(fixed);
400 0           return;
401             }
402 12           fwrite(fixed, 1, out_len, fp);
403 12           fclose(fp);
404             }
405 34 100         if (opts & ESHU_OPT_DIFF) {
406 1           result->diff = eshu_simple_diff(path, src, src_len,
407             fixed, out_len, &result->diff_len);
408             }
409             }
410              
411 36           free(src);
412 36           free(fixed);
413             }
414              
415             /* ══════════════════════════════════════════════════════════════════
416             * Sorted string list (for collecting file paths)
417             * ══════════════════════════════════════════════════════════════════ */
418              
419             typedef struct {
420             char **items;
421             size_t count;
422             size_t cap;
423             } eshu_strlist_t;
424              
425 16           static void eshu_strlist_init(eshu_strlist_t *l) {
426 16           l->cap = 256;
427 16           l->count = 0;
428 16           l->items = (char **)malloc(l->cap * sizeof(char *));
429 16           }
430              
431 46           static void eshu_strlist_push(eshu_strlist_t *l, const char *s) {
432 46 50         if (l->count >= l->cap) {
433 0           l->cap *= 2;
434 0           l->items = (char **)realloc(l->items, l->cap * sizeof(char *));
435             }
436 46           l->items[l->count++] = strdup(s);
437 46           }
438              
439 40           static int eshu_strcmp_ptr(const void *a, const void *b) {
440 40           return strcmp(*(const char **)a, *(const char **)b);
441             }
442              
443 16           static void eshu_strlist_sort(eshu_strlist_t *l) {
444 16           qsort(l->items, l->count, sizeof(char *), eshu_strcmp_ptr);
445 16           }
446              
447 16           static void eshu_strlist_free(eshu_strlist_t *l) {
448             size_t i;
449 62 100         for (i = 0; i < l->count; i++) free(l->items[i]);
450 16           free(l->items);
451 16           l->items = NULL;
452 16           l->count = l->cap = 0;
453 16           }
454              
455             /* ══════════════════════════════════════════════════════════════════
456             * Recursive directory walk
457             * ══════════════════════════════════════════════════════════════════ */
458              
459 22           static void eshu_walk_dir(const char *dir_path, eshu_strlist_t *files,
460             int recursive)
461             {
462             DIR *d;
463             struct dirent *ent;
464             struct stat st;
465             char pathbuf[4096];
466              
467 22           d = opendir(dir_path);
468 22 50         if (!d) return;
469              
470 121 100         while ((ent = readdir(d)) != NULL) {
471 99 100         if (ent->d_name[0] == '.') continue;
472              
473 55           snprintf(pathbuf, sizeof(pathbuf), "%s/%s", dir_path, ent->d_name);
474              
475 55 50         if (lstat(pathbuf, &st) != 0) continue;
476              
477 55 100         if (S_ISLNK(st.st_mode)) {
478             /* Symlink: follow to files only, never to directories */
479             struct stat tgt;
480 4 50         if (stat(pathbuf, &tgt) == 0 && S_ISREG(tgt.st_mode)) {
    100          
481 2           eshu_strlist_push(files, pathbuf);
482             }
483 51 100         } else if (S_ISREG(st.st_mode)) {
484 44           eshu_strlist_push(files, pathbuf);
485 7 100         } else if (recursive && S_ISDIR(st.st_mode)) {
    50          
486 6           eshu_walk_dir(pathbuf, files, recursive);
487             }
488             }
489 22           closedir(d);
490             }
491              
492             /* Check if symlinked dir — lstat shows symlink, stat shows dir */
493 0           static int eshu_is_symlinked_dir(const char *path) {
494             struct stat lst, st;
495 0 0         if (lstat(path, &lst) != 0) return 0;
496 0 0         if (!S_ISLNK(lst.st_mode)) return 0;
497 0 0         if (stat(path, &st) != 0) return 0;
498 0           return S_ISDIR(st.st_mode);
499             }
500              
501             /* ══════════════════════════════════════════════════════════════════
502             * indent_dir — process a directory tree
503             * ══════════════════════════════════════════════════════════════════ */
504              
505             /* Note: exclude/include regex filtering is handled at the XSUB level
506             * since PCRE/regex matching is more natural from Perl. This C function
507             * collects all files and processes them; the XSUB wrapper handles
508             * filtering before calling eshu_indent_file for each file. */
509              
510             #endif /* ESHU_FILE_H */