70322ae3 |
1 | /* |
2 | * html.c: implementation of html.h. |
3 | */ |
4 | |
353bc75d |
5 | #include "agedu.h" |
70322ae3 |
6 | #include "html.h" |
995db599 |
7 | #include "alloc.h" |
70322ae3 |
8 | #include "trie.h" |
9 | #include "index.h" |
10 | |
70322ae3 |
11 | #define MAXCOLOUR 511 |
12 | |
13 | struct html { |
14 | char *buf; |
15 | size_t buflen, bufsize; |
16 | const void *t; |
17 | unsigned long long totalsize, oldest, newest; |
18 | char *path2; |
19 | char *href; |
20 | size_t hreflen; |
21 | const char *format; |
3f940260 |
22 | unsigned long long thresholds[MAXCOLOUR]; |
23 | char *titletexts[MAXCOLOUR+1]; |
70322ae3 |
24 | time_t now; |
25 | }; |
26 | |
27 | static void vhtprintf(struct html *ctx, char *fmt, va_list ap) |
28 | { |
29 | va_list ap2; |
30 | int size, size2; |
50e82fdc |
31 | char testbuf[2]; |
70322ae3 |
32 | |
33 | va_copy(ap2, ap); |
50e82fdc |
34 | /* |
35 | * Some C libraries (Solaris, I'm looking at you) don't like |
36 | * an output buffer size of zero in vsnprintf, but will return |
37 | * sensible values given any non-zero buffer size. Hence, we |
38 | * use testbuf to gauge the length of the string. |
39 | */ |
40 | size = vsnprintf(testbuf, 1, fmt, ap2); |
70322ae3 |
41 | va_end(ap2); |
42 | |
43 | if (ctx->buflen + size >= ctx->bufsize) { |
44 | ctx->bufsize = (ctx->buflen + size) * 3 / 2 + 1024; |
45 | ctx->buf = sresize(ctx->buf, ctx->bufsize, char); |
46 | } |
47 | size2 = vsnprintf(ctx->buf + ctx->buflen, ctx->bufsize - ctx->buflen, |
48 | fmt, ap); |
49 | assert(size == size2); |
50 | ctx->buflen += size; |
51 | } |
52 | |
53 | static void htprintf(struct html *ctx, char *fmt, ...) |
54 | { |
55 | va_list ap; |
56 | va_start(ap, fmt); |
57 | vhtprintf(ctx, fmt, ap); |
58 | va_end(ap); |
59 | } |
60 | |
61 | static unsigned long long round_and_format_age(struct html *ctx, |
62 | unsigned long long age, |
63 | char *buf, int direction) |
64 | { |
65 | struct tm tm, tm2; |
66 | char newbuf[80]; |
67 | unsigned long long ret, newret; |
68 | int i; |
69 | int ym; |
70 | static const int minutes[] = { 5, 10, 15, 30, 45 }; |
71 | |
72 | tm = *localtime(&ctx->now); |
73 | ym = tm.tm_year * 12 + tm.tm_mon; |
74 | |
75 | ret = ctx->now; |
76 | strcpy(buf, "Now"); |
77 | |
78 | for (i = 0; i < lenof(minutes); i++) { |
79 | newret = ctx->now - minutes[i] * 60; |
80 | sprintf(newbuf, "%d minutes", minutes[i]); |
81 | if (newret < age) |
82 | goto finish; |
83 | strcpy(buf, newbuf); |
84 | ret = newret; |
85 | } |
86 | |
87 | for (i = 1; i < 24; i++) { |
88 | newret = ctx->now - i * (60*60); |
89 | sprintf(newbuf, "%d hour%s", i, i==1 ? "" : "s"); |
90 | if (newret < age) |
91 | goto finish; |
92 | strcpy(buf, newbuf); |
93 | ret = newret; |
94 | } |
95 | |
96 | for (i = 1; i < 7; i++) { |
97 | newret = ctx->now - i * (24*60*60); |
98 | sprintf(newbuf, "%d day%s", i, i==1 ? "" : "s"); |
99 | if (newret < age) |
100 | goto finish; |
101 | strcpy(buf, newbuf); |
102 | ret = newret; |
103 | } |
104 | |
105 | for (i = 1; i < 4; i++) { |
106 | newret = ctx->now - i * (7*24*60*60); |
107 | sprintf(newbuf, "%d week%s", i, i==1 ? "" : "s"); |
108 | if (newret < age) |
109 | goto finish; |
110 | strcpy(buf, newbuf); |
111 | ret = newret; |
112 | } |
113 | |
114 | for (i = 1; i < 11; i++) { |
115 | tm2 = tm; /* structure copy */ |
116 | tm2.tm_year = (ym - i) / 12; |
117 | tm2.tm_mon = (ym - i) % 12; |
118 | newret = mktime(&tm2); |
119 | sprintf(newbuf, "%d month%s", i, i==1 ? "" : "s"); |
120 | if (newret < age) |
121 | goto finish; |
122 | strcpy(buf, newbuf); |
123 | ret = newret; |
124 | } |
125 | |
126 | for (i = 1;; i++) { |
127 | tm2 = tm; /* structure copy */ |
128 | tm2.tm_year = (ym - i*12) / 12; |
129 | tm2.tm_mon = (ym - i*12) % 12; |
130 | newret = mktime(&tm2); |
131 | sprintf(newbuf, "%d year%s", i, i==1 ? "" : "s"); |
132 | if (newret < age) |
133 | goto finish; |
134 | strcpy(buf, newbuf); |
135 | ret = newret; |
136 | } |
137 | |
138 | finish: |
139 | if (direction > 0) { |
140 | /* |
141 | * Round toward newest, i.e. use the existing (buf,ret). |
142 | */ |
143 | } else if (direction < 0) { |
144 | /* |
145 | * Round toward oldest, i.e. use (newbuf,newret); |
146 | */ |
147 | strcpy(buf, newbuf); |
148 | ret = newret; |
149 | } else { |
150 | /* |
151 | * Round to nearest. |
152 | */ |
153 | if (ret - age > age - newret) { |
154 | strcpy(buf, newbuf); |
155 | ret = newret; |
156 | } |
157 | } |
158 | return ret; |
159 | } |
160 | |
161 | static void get_indices(const void *t, char *path, |
162 | unsigned long *xi1, unsigned long *xi2) |
163 | { |
164 | size_t pathlen = strlen(path); |
256c29a2 |
165 | int c1 = path[pathlen], c2 = (pathlen > 0 ? path[pathlen-1] : 0); |
70322ae3 |
166 | |
167 | *xi1 = trie_before(t, path); |
256c29a2 |
168 | make_successor(path); |
70322ae3 |
169 | *xi2 = trie_before(t, path); |
256c29a2 |
170 | path[pathlen] = c1; |
171 | if (pathlen > 0) |
172 | path[pathlen-1] = c2; |
70322ae3 |
173 | } |
174 | |
3f940260 |
175 | static unsigned long long fetch_size(const void *t, |
176 | unsigned long xi1, unsigned long xi2, |
70322ae3 |
177 | unsigned long long atime) |
178 | { |
70322ae3 |
179 | return index_query(t, xi2, atime) - index_query(t, xi1, atime); |
180 | } |
181 | |
182 | static void htescape(struct html *ctx, const char *s, int n, int italics) |
183 | { |
184 | while (n > 0 && *s) { |
185 | unsigned char c = (unsigned char)*s++; |
186 | |
187 | if (c == '&') |
188 | htprintf(ctx, "&"); |
189 | else if (c == '<') |
190 | htprintf(ctx, "<"); |
191 | else if (c == '>') |
192 | htprintf(ctx, ">"); |
193 | else if (c >= ' ' && c < '\177') |
194 | htprintf(ctx, "%c", c); |
195 | else { |
196 | if (italics) htprintf(ctx, "<i>"); |
197 | htprintf(ctx, "[%02x]", c); |
198 | if (italics) htprintf(ctx, "</i>"); |
199 | } |
200 | |
201 | n--; |
202 | } |
203 | } |
204 | |
205 | static void begin_colour_bar(struct html *ctx) |
206 | { |
207 | htprintf(ctx, "<table cellspacing=0 cellpadding=0" |
208 | " style=\"border:0\">\n<tr>\n"); |
209 | } |
210 | |
211 | static void add_to_colour_bar(struct html *ctx, int colour, int pixels) |
212 | { |
213 | int r, g, b; |
70322ae3 |
214 | |
215 | if (colour >= 0 && colour < 256) /* red -> yellow fade */ |
216 | r = 255, g = colour, b = 0; |
217 | else if (colour >= 256 && colour <= 511) /* yellow -> green fade */ |
218 | r = 511 - colour, g = 255, b = 0; |
219 | else /* background grey */ |
220 | r = g = b = 240; |
221 | |
70322ae3 |
222 | if (pixels > 0) { |
223 | htprintf(ctx, "<td style=\"width:%dpx; height:1em; " |
224 | "background-color:#%02x%02x%02x\"", |
225 | pixels, r, g, b); |
226 | if (colour >= 0) |
3f940260 |
227 | htprintf(ctx, " title=\"%s\"", ctx->titletexts[colour]); |
70322ae3 |
228 | htprintf(ctx, "></td>\n"); |
229 | } |
230 | } |
231 | |
232 | static void end_colour_bar(struct html *ctx) |
233 | { |
234 | htprintf(ctx, "</tr>\n</table>\n"); |
235 | } |
236 | |
237 | struct vector { |
238 | int want_href; |
239 | char *name; |
240 | unsigned long index; |
241 | unsigned long long sizes[MAXCOLOUR+1]; |
242 | }; |
243 | |
244 | int vec_compare(const void *av, const void *bv) |
245 | { |
246 | const struct vector *a = *(const struct vector **)av; |
247 | const struct vector *b = *(const struct vector **)bv; |
248 | |
249 | if (a->sizes[MAXCOLOUR] > b->sizes[MAXCOLOUR]) |
250 | return -1; |
251 | else if (a->sizes[MAXCOLOUR] < b->sizes[MAXCOLOUR]) |
252 | return +1; |
253 | else if (a->want_href < b->want_href) |
254 | return +1; |
255 | else if (a->want_href > b->want_href) |
256 | return -1; |
257 | else if (a->want_href) |
258 | return strcmp(a->name, b->name); |
259 | else if (a->index < b->index) |
260 | return -1; |
261 | else if (a->index > b->index) |
262 | return +1; |
263 | return 0; |
264 | } |
265 | |
266 | static struct vector *make_vector(struct html *ctx, char *path, |
267 | int want_href, char *name) |
268 | { |
269 | unsigned long xi1, xi2; |
270 | struct vector *vec = snew(struct vector); |
271 | int i; |
272 | |
273 | vec->want_href = want_href; |
274 | vec->name = name ? dupstr(name) : NULL; |
275 | |
276 | get_indices(ctx->t, path, &xi1, &xi2); |
277 | |
278 | vec->index = xi1; |
279 | |
280 | for (i = 0; i <= MAXCOLOUR; i++) { |
281 | unsigned long long atime; |
282 | if (i == MAXCOLOUR) |
283 | atime = ULLONG_MAX; |
284 | else |
285 | atime = ctx->thresholds[i]; |
3f940260 |
286 | vec->sizes[i] = fetch_size(ctx->t, xi1, xi2, atime); |
70322ae3 |
287 | } |
288 | |
289 | return vec; |
290 | } |
291 | |
292 | static void print_heading(struct html *ctx, const char *title) |
293 | { |
294 | htprintf(ctx, "<tr style=\"padding: 0.2em; background-color:#e0e0e0\">\n" |
295 | "<td colspan=4 align=center>%s</td>\n</tr>\n", title); |
296 | } |
297 | |
298 | #define PIXEL_SIZE 600 /* FIXME: configurability? */ |
299 | static void write_report_line(struct html *ctx, struct vector *vec) |
300 | { |
742c1a74 |
301 | unsigned long long size, asize, divisor; |
70322ae3 |
302 | int pix, newpix; |
303 | int i; |
304 | |
305 | /* |
010dd2a2 |
306 | * A line with literally zero space usage should not be |
307 | * printed at all if it's a link to a subdirectory (since it |
308 | * probably means the whole thing was excluded by some |
309 | * --exclude-path wildcard). If it's [files] or the top-level |
310 | * line, though, we must always print _something_, and in that |
311 | * case we must fiddle about to prevent divisions by zero in |
312 | * the code below. |
742c1a74 |
313 | */ |
010dd2a2 |
314 | if (!vec->sizes[MAXCOLOUR] && vec->want_href) |
315 | return; |
742c1a74 |
316 | divisor = ctx->totalsize; |
010dd2a2 |
317 | if (!divisor) { |
742c1a74 |
318 | divisor = 1; |
010dd2a2 |
319 | } |
742c1a74 |
320 | |
321 | /* |
70322ae3 |
322 | * Find the total size of this subdirectory. |
323 | */ |
324 | size = vec->sizes[MAXCOLOUR]; |
325 | htprintf(ctx, "<tr>\n" |
326 | "<td style=\"padding: 0.2em; text-align: right\">%lluMb</td>\n", |
84849cbd |
327 | ((size + ((1<<20)-1)) >> 20)); /* convert to Mb, rounding up */ |
70322ae3 |
328 | |
329 | /* |
330 | * Generate a colour bar. |
331 | */ |
332 | htprintf(ctx, "<td style=\"padding: 0.2em\">\n"); |
333 | begin_colour_bar(ctx); |
334 | pix = 0; |
335 | for (i = 0; i <= MAXCOLOUR; i++) { |
336 | asize = vec->sizes[i]; |
742c1a74 |
337 | newpix = asize * PIXEL_SIZE / divisor; |
70322ae3 |
338 | add_to_colour_bar(ctx, i, newpix - pix); |
339 | pix = newpix; |
340 | } |
341 | add_to_colour_bar(ctx, -1, PIXEL_SIZE - pix); |
342 | end_colour_bar(ctx); |
343 | htprintf(ctx, "</td>\n"); |
344 | |
345 | /* |
346 | * Output size as a percentage of totalsize. |
347 | */ |
348 | htprintf(ctx, "<td style=\"padding: 0.2em; text-align: right\">" |
742c1a74 |
349 | "%.2f%%</td>\n", (double)size / divisor * 100.0); |
70322ae3 |
350 | |
351 | /* |
352 | * Output a subdirectory marker. |
353 | */ |
354 | htprintf(ctx, "<td style=\"padding: 0.2em\">"); |
355 | if (vec->name) { |
356 | int doing_href = 0; |
357 | |
358 | if (ctx->format && vec->want_href) { |
359 | snprintf(ctx->href, ctx->hreflen, ctx->format, vec->index); |
360 | htprintf(ctx, "<a href=\"%s\">", ctx->href); |
361 | doing_href = 1; |
362 | } |
363 | htescape(ctx, vec->name, strlen(vec->name), 1); |
364 | if (doing_href) |
365 | htprintf(ctx, "</a>"); |
366 | } |
367 | htprintf(ctx, "</td>\n</tr>\n"); |
368 | } |
369 | |
0089cdbb |
370 | int strcmptrailingpathsep(const char *a, const char *b) |
371 | { |
372 | while (*a == *b && *a) |
373 | a++, b++; |
374 | |
375 | if ((*a == pathsep && !a[1] && !*b) || |
376 | (*b == pathsep && !b[1] && !*a)) |
377 | return 0; |
378 | |
379 | return (int)(unsigned char)*a - (int)(unsigned char)*b; |
380 | } |
381 | |
f2e52893 |
382 | char *html_query(const void *t, unsigned long index, |
383 | const struct html_config *cfg) |
70322ae3 |
384 | { |
385 | struct html actx, *ctx = &actx; |
386 | char *path, *path2, *p, *q, *href; |
387 | char agebuf1[80], agebuf2[80]; |
256c29a2 |
388 | size_t pathlen, subdirpos, hreflen; |
70322ae3 |
389 | unsigned long index2; |
390 | int i; |
391 | struct vector **vecs; |
392 | int nvecs, vecsize; |
393 | unsigned long xi1, xi2, xj1, xj2; |
394 | |
395 | if (index >= trie_count(t)) |
396 | return NULL; |
397 | |
398 | ctx->buf = NULL; |
399 | ctx->buflen = ctx->bufsize = 0; |
400 | ctx->t = t; |
f2e52893 |
401 | ctx->format = cfg->format; |
70322ae3 |
402 | htprintf(ctx, "<html>\n"); |
403 | |
404 | path = snewn(1+trie_maxpathlen(t), char); |
405 | ctx->path2 = path2 = snewn(1+trie_maxpathlen(t), char); |
f2e52893 |
406 | if (cfg->format) { |
407 | hreflen = strlen(cfg->format) + 100; |
70322ae3 |
408 | href = snewn(hreflen, char); |
409 | } else { |
410 | hreflen = 0; |
411 | href = NULL; |
412 | } |
413 | ctx->hreflen = hreflen; |
414 | ctx->href = href; |
415 | |
416 | /* |
417 | * HEAD section. |
418 | */ |
419 | htprintf(ctx, "<head>\n"); |
420 | trie_getpath(t, index, path); |
bf53e756 |
421 | htprintf(ctx, "<title>%s: ", PNAME); |
70322ae3 |
422 | htescape(ctx, path, strlen(path), 0); |
423 | htprintf(ctx, "</title>\n"); |
424 | htprintf(ctx, "</head>\n"); |
425 | |
426 | /* |
427 | * Begin BODY section. |
428 | */ |
429 | htprintf(ctx, "<body>\n"); |
430 | htprintf(ctx, "<h3 align=center>Disk space breakdown by" |
431 | " last-access time</h3>\n"); |
432 | |
433 | /* |
434 | * Show the pathname we're centred on, with hyperlinks to |
435 | * parent directories where available. |
436 | */ |
437 | htprintf(ctx, "<p align=center>\n<code>"); |
438 | q = path; |
cfe942fb |
439 | for (p = strchr(path, pathsep); p && p[1]; p = strchr(p, pathsep)) { |
70322ae3 |
440 | int doing_href = 0; |
256c29a2 |
441 | char c, *zp; |
442 | |
70322ae3 |
443 | /* |
444 | * See if this path prefix exists in the trie. If so, |
445 | * generate a hyperlink. |
446 | */ |
256c29a2 |
447 | zp = p; |
448 | if (p == path) /* special case for "/" at start */ |
449 | zp++; |
450 | |
451 | p++; |
452 | |
453 | c = *zp; |
454 | *zp = '\0'; |
70322ae3 |
455 | index2 = trie_before(t, path); |
456 | trie_getpath(t, index2, path2); |
0089cdbb |
457 | if (!strcmptrailingpathsep(path, path2) && cfg->format) { |
f2e52893 |
458 | snprintf(href, hreflen, cfg->format, index2); |
cfe942fb |
459 | if (!*href) /* special case that we understand */ |
460 | strcpy(href, "./"); |
70322ae3 |
461 | htprintf(ctx, "<a href=\"%s\">", href); |
462 | doing_href = 1; |
463 | } |
256c29a2 |
464 | *zp = c; |
465 | htescape(ctx, q, zp - q, 1); |
70322ae3 |
466 | if (doing_href) |
467 | htprintf(ctx, "</a>"); |
256c29a2 |
468 | htescape(ctx, zp, p - zp, 1); |
469 | q = p; |
70322ae3 |
470 | } |
471 | htescape(ctx, q, strlen(q), 1); |
472 | htprintf(ctx, "</code>\n"); |
473 | |
474 | /* |
475 | * Decide on the age limit of our colour coding, establish the |
476 | * colour thresholds, and write out a key. |
477 | */ |
70322ae3 |
478 | ctx->now = time(NULL); |
f2e52893 |
479 | if (cfg->autoage) { |
480 | ctx->oldest = index_order_stat(t, 0.05); |
481 | ctx->newest = index_order_stat(t, 1.0); |
482 | ctx->oldest = round_and_format_age(ctx, ctx->oldest, agebuf1, -1); |
483 | ctx->newest = round_and_format_age(ctx, ctx->newest, agebuf2, +1); |
484 | } else { |
485 | ctx->oldest = cfg->oldest; |
486 | ctx->newest = cfg->newest; |
487 | ctx->oldest = round_and_format_age(ctx, ctx->oldest, agebuf1, 0); |
488 | ctx->newest = round_and_format_age(ctx, ctx->newest, agebuf2, 0); |
489 | } |
3f940260 |
490 | for (i = 0; i < MAXCOLOUR; i++) { |
70322ae3 |
491 | ctx->thresholds[i] = |
3f940260 |
492 | ctx->oldest + (ctx->newest - ctx->oldest) * i / (MAXCOLOUR-1); |
493 | } |
494 | for (i = 0; i <= MAXCOLOUR; i++) { |
495 | char buf[80]; |
496 | |
497 | if (i == 0) { |
498 | strcpy(buf, "< "); |
499 | round_and_format_age(ctx, ctx->thresholds[0], buf+5, 0); |
500 | } else if (i == MAXCOLOUR) { |
501 | strcpy(buf, "> "); |
502 | round_and_format_age(ctx, ctx->thresholds[MAXCOLOUR-1], buf+5, 0); |
503 | } else { |
504 | unsigned long long midrange = |
505 | (ctx->thresholds[i-1] + ctx->thresholds[i]) / 2; |
506 | round_and_format_age(ctx, midrange, buf, 0); |
507 | } |
508 | |
509 | ctx->titletexts[i] = dupstr(buf); |
70322ae3 |
510 | } |
511 | htprintf(ctx, "<p align=center>Key to colour coding (mouse over for more detail):\n"); |
512 | htprintf(ctx, "<p align=center style=\"padding: 0; margin-top:0.4em; " |
513 | "margin-bottom:1em\""); |
514 | begin_colour_bar(ctx); |
515 | htprintf(ctx, "<td style=\"padding-right:1em\">%s</td>\n", agebuf1); |
516 | for (i = 0; i < MAXCOLOUR; i++) |
517 | add_to_colour_bar(ctx, i, 1); |
518 | htprintf(ctx, "<td style=\"padding-left:1em\">%s</td>\n", agebuf2); |
519 | end_colour_bar(ctx); |
520 | |
521 | /* |
522 | * Begin the main table. |
523 | */ |
524 | htprintf(ctx, "<p align=center>\n<table style=\"margin:0; border:0\">\n"); |
525 | |
526 | /* |
527 | * Find the total size of our entire subdirectory. We'll use |
528 | * that as the scale for all the colour bars in this report. |
529 | */ |
3f940260 |
530 | get_indices(t, path, &xi1, &xi2); |
531 | ctx->totalsize = fetch_size(t, xi1, xi2, ULLONG_MAX); |
70322ae3 |
532 | |
533 | /* |
534 | * Generate a report line for the whole subdirectory. |
535 | */ |
536 | vecsize = 64; |
537 | vecs = snewn(vecsize, struct vector *); |
538 | nvecs = 1; |
539 | vecs[0] = make_vector(ctx, path, 0, NULL); |
540 | print_heading(ctx, "Overall"); |
541 | write_report_line(ctx, vecs[0]); |
542 | |
543 | /* |
544 | * Now generate report lines for all its children, and the |
545 | * files contained in it. |
546 | */ |
547 | print_heading(ctx, "Subdirectories"); |
548 | |
549 | vecs[0]->name = dupstr("[files]"); |
550 | get_indices(t, path, &xi1, &xi2); |
551 | xi1++; |
552 | pathlen = strlen(path); |
256c29a2 |
553 | subdirpos = pathlen + 1; |
554 | if (pathlen > 0 && path[pathlen-1] == pathsep) |
555 | subdirpos--; |
70322ae3 |
556 | while (xi1 < xi2) { |
557 | trie_getpath(t, xi1, path2); |
558 | get_indices(t, ctx->path2, &xj1, &xj2); |
559 | xi1 = xj2; |
560 | if (xj2 - xj1 <= 1) |
561 | continue; /* skip individual files */ |
562 | if (nvecs >= vecsize) { |
563 | vecsize = nvecs * 3 / 2 + 64; |
564 | vecs = sresize(vecs, vecsize, struct vector *); |
565 | } |
566 | assert(strlen(path2) > pathlen); |
256c29a2 |
567 | vecs[nvecs] = make_vector(ctx, path2, 1, path2 + subdirpos); |
70322ae3 |
568 | for (i = 0; i <= MAXCOLOUR; i++) |
569 | vecs[0]->sizes[i] -= vecs[nvecs]->sizes[i]; |
570 | nvecs++; |
571 | } |
572 | |
573 | qsort(vecs, nvecs, sizeof(vecs[0]), vec_compare); |
574 | |
575 | for (i = 0; i < nvecs; i++) |
576 | write_report_line(ctx, vecs[i]); |
577 | |
578 | /* |
579 | * Close the main table. |
580 | */ |
581 | htprintf(ctx, "</table>\n"); |
582 | |
583 | /* |
584 | * Finish up and tidy up. |
585 | */ |
586 | htprintf(ctx, "</body>\n"); |
587 | htprintf(ctx, "</html>\n"); |
588 | sfree(href); |
589 | sfree(path2); |
590 | sfree(path); |
591 | for (i = 0; i < nvecs; i++) { |
592 | sfree(vecs[i]->name); |
593 | sfree(vecs[i]); |
594 | } |
595 | sfree(vecs); |
596 | |
597 | return ctx->buf; |
598 | } |