Compare commits

...

4 Commits

Author SHA1 Message Date
Xavier Roche
348a7d8cb2 Return HTTP status reason phrases via a const-returning switch
infostatuscode() was a ~60-case switch, each arm strcpybuff()-ing a literal into
the caller's char* msg: 42 unchecked pointer-destination copies of static data.
Keep the same O(1) switch dispatch but have it return the phrase instead of
copying -- new public infostatuscode_const(int) -> const char* (or NULL) -- and
do the copy in a thin wrapper.

infostatuscode() preserves exact behavior: a known code overwrites msg; an
unknown code keeps any caller-provided message, else writes "Unknown error".
The single remaining copy uses strlcpybuff with the documented 64-byte minimum
(longest phrase is 31; all callers pass >= 80).

Drops 42 pointer-destination warnings (htslib.c 56 -> 14; tree 179 -> 137).
No dispatch regression: it stays a switch (jump table), no allocation, no
per-call scan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 13:14:23 +02:00
Xavier Roche
5f81741ac5 Merge pull request #332 from xroche/cleanup/url_savename-htsbuff
Convert the url_savename template renderer to htsbuff
2026-06-14 13:01:32 +02:00
Xavier Roche
0cf14c4e88 Convert the url_savename template renderer to htsbuff
The savename_type == -1 userdef renderer walked afs->save with a raw char*
cursor, doing "b += strlen(b)" after each write, and strcpybuff(b, ...) on that
char* was unchecked (the pointer-destination case). That manual pointer math is
where the function's off-by-one / strlen-based hazards lived.

Convert the cursor to an htsbuff over afs->save (capacity sizeof = the full
HTS_URLMAXSIZE*2 buffer): every append is now bounds-checked and the pointer
math is gone. The loop's truncation guard becomes "sb.len < HTS_URLMAXSIZE",
preserving the existing cap-at-1024 behavior; the 2x buffer means a write only
aborts where it would previously have overrun. Add htsbuff_catc for the
single-character appends ('%', '.', literal copy).

Removes 35 pointer-destination warnings (htsname.c 51 -> 9; the renderer is now
warning-free). Behavior verified identical: the pre-change and new binaries
produce byte-identical output across 14 -N templates (%n %N %t %p %h %H %M %q %r
%% %[param], the short %s variants, and literals) crawling a local site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 12:59:29 +02:00
Xavier Roche
29a07ff487 Merge pull request #334 from xroche/cleanup/git-format-hook
Add an opt-in pre-commit hook that auto-formats changed C lines
2026-06-14 12:58:42 +02:00
5 changed files with 145 additions and 200 deletions

View File

@@ -211,6 +211,10 @@ static int string_safety_selftests(void) {
htsbuff_cpy(&b, "xyz"); /* reset */ htsbuff_cpy(&b, "xyz"); /* reset */
if (strcmp(htsbuff_str(&b), "xyz") != 0 || b.len != 3) if (strcmp(htsbuff_str(&b), "xyz") != 0 || b.len != 3)
return 1; return 1;
htsbuff_catc(&b, '!'); /* single character */
if (strcmp(htsbuff_str(&b), "xyz!") != 0 || b.len != 4)
return 1;
} }
/* boundary: filling to exactly cap-1 must succeed (one more aborts, which the /* boundary: filling to exactly cap-1 must succeed (one more aborts, which the

View File

@@ -1660,138 +1660,107 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
} }
} }
// transforme le message statuscode en chaîne // HTTP status code -> reason phrase (per RFC), or NULL if unknown.
HTSEXT_API void infostatuscode(char *msg, int statuscode) { HTSEXT_API const char *infostatuscode_const(int statuscode) {
// O(1) dispatch (the compiler builds a jump table); the phrases are static.
switch (statuscode) { switch (statuscode) {
// Erreurs HTTP, selon RFC
case 100: case 100:
strcpybuff(msg, "Continue"); return "Continue";
break;
case 101: case 101:
strcpybuff(msg, "Switching Protocols"); return "Switching Protocols";
break;
case 200: case 200:
strcpybuff(msg, "OK"); return "OK";
break;
case 201: case 201:
strcpybuff(msg, "Created"); return "Created";
break;
case 202: case 202:
strcpybuff(msg, "Accepted"); return "Accepted";
break;
case 203: case 203:
strcpybuff(msg, "Non-Authoritative Information"); return "Non-Authoritative Information";
break;
case 204: case 204:
strcpybuff(msg, "No Content"); return "No Content";
break;
case 205: case 205:
strcpybuff(msg, "Reset Content"); return "Reset Content";
break;
case 206: case 206:
strcpybuff(msg, "Partial Content"); return "Partial Content";
break;
case 300: case 300:
strcpybuff(msg, "Multiple Choices"); return "Multiple Choices";
break;
case 301: case 301:
strcpybuff(msg, "Moved Permanently"); return "Moved Permanently";
break;
case 302: case 302:
strcpybuff(msg, "Moved Temporarily"); return "Moved Temporarily";
break;
case 303: case 303:
strcpybuff(msg, "See Other"); return "See Other";
break;
case 304: case 304:
strcpybuff(msg, "Not Modified"); return "Not Modified";
break;
case 305: case 305:
strcpybuff(msg, "Use Proxy"); return "Use Proxy";
break;
case 306: case 306:
strcpybuff(msg, "Undefined 306 error"); return "Undefined 306 error";
break;
case 307: case 307:
strcpybuff(msg, "Temporary Redirect"); return "Temporary Redirect";
break;
case 400: case 400:
strcpybuff(msg, "Bad Request"); return "Bad Request";
break;
case 401: case 401:
strcpybuff(msg, "Unauthorized"); return "Unauthorized";
break;
case 402: case 402:
strcpybuff(msg, "Payment Required"); return "Payment Required";
break;
case 403: case 403:
strcpybuff(msg, "Forbidden"); return "Forbidden";
break;
case 404: case 404:
strcpybuff(msg, "Not Found"); return "Not Found";
break;
case 405: case 405:
strcpybuff(msg, "Method Not Allowed"); return "Method Not Allowed";
break;
case 406: case 406:
strcpybuff(msg, "Not Acceptable"); return "Not Acceptable";
break;
case 407: case 407:
strcpybuff(msg, "Proxy Authentication Required"); return "Proxy Authentication Required";
break;
case 408: case 408:
strcpybuff(msg, "Request Time-out"); return "Request Time-out";
break;
case 409: case 409:
strcpybuff(msg, "Conflict"); return "Conflict";
break;
case 410: case 410:
strcpybuff(msg, "Gone"); return "Gone";
break;
case 411: case 411:
strcpybuff(msg, "Length Required"); return "Length Required";
break;
case 412: case 412:
strcpybuff(msg, "Precondition Failed"); return "Precondition Failed";
break;
case 413: case 413:
strcpybuff(msg, "Request Entity Too Large"); return "Request Entity Too Large";
break;
case 414: case 414:
strcpybuff(msg, "Request-URI Too Large"); return "Request-URI Too Large";
break;
case 415: case 415:
strcpybuff(msg, "Unsupported Media Type"); return "Unsupported Media Type";
break;
case 416: case 416:
strcpybuff(msg, "Requested Range Not Satisfiable"); return "Requested Range Not Satisfiable";
break;
case 417: case 417:
strcpybuff(msg, "Expectation Failed"); return "Expectation Failed";
break;
case 500: case 500:
strcpybuff(msg, "Internal Server Error"); return "Internal Server Error";
break;
case 501: case 501:
strcpybuff(msg, "Not Implemented"); return "Not Implemented";
break;
case 502: case 502:
strcpybuff(msg, "Bad Gateway"); return "Bad Gateway";
break;
case 503: case 503:
strcpybuff(msg, "Service Unavailable"); return "Service Unavailable";
break;
case 504: case 504:
strcpybuff(msg, "Gateway Time-out"); return "Gateway Time-out";
break;
case 505: case 505:
strcpybuff(msg, "HTTP Version Not Supported"); return "HTTP Version Not Supported";
break;
//
default: default:
if (strnotempty(msg) == 0) return NULL;
strcpybuff(msg, "Unknown error"); }
break; }
// Write the status code's reason phrase into msg. For an unknown code, keep any
// caller-provided message, otherwise fall back to a default. Callers provide a
// buffer of at least 64 bytes (the longest reason phrase is 31).
HTSEXT_API void infostatuscode(char *msg, int statuscode) {
const char *const text = infostatuscode_const(statuscode);
if (text != NULL) {
strlcpybuff(msg, text, 64);
} else if (strnotempty(msg) == 0) {
strlcpybuff(msg, "Unknown error", 64);
} }
} }

View File

@@ -767,7 +767,7 @@ int url_savename(lien_adrfilsave *const afs,
// ajouter nom du site éventuellement en premier // ajouter nom du site éventuellement en premier
if (opt->savename_type == -1) { // utiliser savename_userdef! (%h%p/%n%q.%t) if (opt->savename_type == -1) { // utiliser savename_userdef! (%h%p/%n%q.%t)
const char *a = StringBuff(opt->savename_userdef); const char *a = StringBuff(opt->savename_userdef);
char *b = afs->save; htsbuff sb = htsbuff_array(afs->save);
/*char *nom_pos=NULL,*dot_pos=NULL; // Position nom et point */ /*char *nom_pos=NULL,*dot_pos=NULL; // Position nom et point */
char tok; char tok;
@@ -787,17 +787,16 @@ int url_savename(lien_adrfilsave *const afs,
} }
*/ */
// Construire nom // build the name
while((*a) && (((int) (b - afs->save)) < HTS_URLMAXSIZE)) { // parser, et pas trop long.. while ((*a) && (sb.len < HTS_URLMAXSIZE)) { // parse, but not too long
if (*a == '%') { if (*a == '%') {
int short_ver = 0; int short_ver = 0;
a++; a++;
if (*a == 's') { if (*a == 's') { // '%s...' selects the short (8.3) form
short_ver = 1; short_ver = 1;
a++; a++;
} }
*b = '\0';
switch (tok = *a++) { switch (tok = *a++) {
case '[': // %[param:prefix_if_not_empty:suffix_if_not_empty:empty_replacement:notfound_replacement] case '[': // %[param:prefix_if_not_empty:suffix_if_not_empty:empty_replacement:notfound_replacement]
if (strchr(a, ']')) { if (strchr(a, ']')) {
@@ -834,8 +833,7 @@ int url_savename(lien_adrfilsave *const afs,
} }
if (cp) { if (cp) {
c = cp + strlen(name[0]); /* jumps "param=" */ c = cp + strlen(name[0]); /* jumps "param=" */
strcpybuff(b, name[1]); /* prefix */ htsbuff_cat(&sb, name[1]); /* prefix */
b += strlen(b);
if (*c != '\0' && *c != '&') { if (*c != '\0' && *c != '&') {
char *d = name[0]; char *d = name[0];
@@ -846,110 +844,90 @@ int url_savename(lien_adrfilsave *const afs,
*d = '\0'; *d = '\0';
d = unescape_http(catbuff, sizeof(catbuff), name[0]); d = unescape_http(catbuff, sizeof(catbuff), name[0]);
if (d && *d) { if (d && *d) {
strcpybuff(b, d); /* value */ htsbuff_cat(&sb, d); /* value */
b += strlen(b);
} else { } else {
strcpybuff(b, name[3]); /* empty replacement if any */ htsbuff_cat(&sb, name[3]); /* empty replacement if any */
b += strlen(b);
} }
} else { } else {
strcpybuff(b, name[3]); /* empty replacement if any */ htsbuff_cat(&sb, name[3]); /* empty replacement if any */
b += strlen(b);
} }
strcpybuff(b, name[2]); /* suffix */ htsbuff_cat(&sb, name[2]); /* suffix */
b += strlen(b);
} else { } else {
strcpybuff(b, name[4]); /* not found replacement if any */ htsbuff_cat(&sb, name[4]); /* not found replacement if any */
b += strlen(b);
} }
} else { } else {
strcpybuff(b, name[4]); /* not found replacement if any */ htsbuff_cat(&sb, name[4]); /* not found replacement if any */
b += strlen(b);
} }
} }
break; break;
case '%': case '%':
*b++ = '%'; htsbuff_catc(&sb, '%');
break; break;
case 'n': // nom sans ext case 'n': // name without extension
*b = '\0';
if (dot_pos) { if (dot_pos) {
if (!short_ver) // Noms longs if (!short_ver)
strncatbuff(b, nom_pos, (int) (dot_pos - nom_pos)); htsbuff_catn(&sb, nom_pos, (int) (dot_pos - nom_pos));
else else
strncatbuff(b, nom_pos, min((int) (dot_pos - nom_pos), 8)); htsbuff_catn(&sb, nom_pos, min((int) (dot_pos - nom_pos), 8));
} else { } else {
if (!short_ver) // Noms longs if (!short_ver)
strcpybuff(b, nom_pos); htsbuff_cat(&sb, nom_pos);
else else
strncatbuff(b, nom_pos, 8); htsbuff_catn(&sb, nom_pos, 8);
} }
b += strlen(b); // pointer à la fin
break; break;
case 'N': // nom avec ext case 'N': // name with extension
// RECOPIE NOM + EXT
*b = '\0';
if (dot_pos) { if (dot_pos) {
if (!short_ver) // Noms longs if (!short_ver)
strncatbuff(b, nom_pos, (int) (dot_pos - nom_pos)); htsbuff_catn(&sb, nom_pos, (int) (dot_pos - nom_pos));
else else
strncatbuff(b, nom_pos, min((int) (dot_pos - nom_pos), 8)); htsbuff_catn(&sb, nom_pos, min((int) (dot_pos - nom_pos), 8));
} else { } else {
if (!short_ver) // Noms longs if (!short_ver)
strcpybuff(b, nom_pos); htsbuff_cat(&sb, nom_pos);
else else
strncatbuff(b, nom_pos, 8); htsbuff_catn(&sb, nom_pos, 8);
} }
b += strlen(b); // pointer à la fin htsbuff_catc(&sb, '.');
*b = '.';
++b;
// RECOPIE NOM + EXT
*b = '\0';
if (dot_pos) { if (dot_pos) {
if (!short_ver) // Noms longs if (!short_ver)
strcpybuff(b, dot_pos + 1); htsbuff_cat(&sb, dot_pos + 1);
else else
strncatbuff(b, dot_pos + 1, 3); htsbuff_catn(&sb, dot_pos + 1, 3);
} else { } else {
if (!short_ver) // Noms longs if (!short_ver)
strcpybuff(b, DEFAULT_EXT + 1); // pas de.. htsbuff_cat(&sb, DEFAULT_EXT + 1); // skip the leading dot
else else
strcpybuff(b, DEFAULT_EXT_SHORT + 1); // pas de.. htsbuff_cat(&sb, DEFAULT_EXT_SHORT + 1); // skip the leading dot
} }
b += strlen(b); // pointer à la fin
//
break; break;
case 't': // ext case 't': // extension
*b = '\0';
if (dot_pos) { if (dot_pos) {
if (!short_ver) // Noms longs if (!short_ver)
strcpybuff(b, dot_pos + 1); htsbuff_cat(&sb, dot_pos + 1);
else else
strncatbuff(b, dot_pos + 1, 3); htsbuff_catn(&sb, dot_pos + 1, 3);
} else { } else {
if (!short_ver) // Noms longs if (!short_ver)
strcpybuff(b, DEFAULT_EXT + 1); // pas de.. htsbuff_cat(&sb, DEFAULT_EXT + 1); // skip the leading dot
else else
strcpybuff(b, DEFAULT_EXT_SHORT + 1); // pas de.. htsbuff_cat(&sb, DEFAULT_EXT_SHORT + 1); // skip the leading dot
} }
b += strlen(b); // pointer à la fin
break; break;
case 'p': // path sans dernier / case 'p': // path without trailing /
*b = '\0'; if (nom_pos !=
if (nom_pos != fil + 1) { // pas: /index.html (chemin nul) fil + 1) { // skip when the path is empty (e.g. /index.html)
if (!short_ver) { // Noms longs if (!short_ver) {
strncatbuff(b, fil, (int) (nom_pos - fil) - 1); htsbuff_catn(&sb, fil, (int) (nom_pos - fil) - 1);
} else { } else {
char BIGSTK pth[HTS_URLMAXSIZE * 2], n83[HTS_URLMAXSIZE * 2]; char BIGSTK pth[HTS_URLMAXSIZE * 2], n83[HTS_URLMAXSIZE * 2];
pth[0] = n83[0] = '\0'; pth[0] = n83[0] = '\0';
//
strncatbuff(pth, fil, (int) (nom_pos - fil) - 1); strncatbuff(pth, fil, (int) (nom_pos - fil) - 1);
long_to_83(opt->savename_83, n83, pth); long_to_83(opt->savename_83, n83, pth);
strcpybuff(b, n83); htsbuff_cat(&sb, n83);
} }
} }
b += strlen(b); // pointer à la fin
break; break;
case 'h': // host (IDNA decoded if suitable) case 'h': // host (IDNA decoded if suitable)
// IDNA / RFC 3492 (Punycode) handling for HTTP(s) // IDNA / RFC 3492 (Punycode) handling for HTTP(s)
@@ -957,62 +935,50 @@ int url_savename(lien_adrfilsave *const afs,
DECLARE_ADR(final_adr); DECLARE_ADR(final_adr);
/* Copy address */ /* Copy address */
*b = '\0';
if (!short_ver) if (!short_ver)
strcpybuff(b, final_adr); htsbuff_cat(&sb, final_adr);
else else
strcpybuff(b, final_adr); htsbuff_cat(&sb, final_adr);
/* release */ /* release */
RELEASE_ADR(); RELEASE_ADR();
} }
b += strlen(b); // pointer à la fin
break; break;
case 'H': // host, raw (old mode) case 'H': // host, raw (old mode)
*b = '\0';
if (protocol == PROTOCOL_FILE) { if (protocol == PROTOCOL_FILE) {
if (!short_ver) // Noms longs if (!short_ver)
strcpybuff(b, "localhost"); htsbuff_cat(&sb, "localhost");
else else
strcpybuff(b, "local"); htsbuff_cat(&sb, "local");
} else { } else {
if (!short_ver) // Noms longs if (!short_ver)
strcpybuff(b, print_adr); htsbuff_cat(&sb, print_adr);
else else
strncatbuff(b, print_adr, 8); htsbuff_catn(&sb, print_adr, 8);
} }
b += strlen(b); // pointer à la fin
break; break;
case 'M': /* host/address?query MD5 (128-bits) */ case 'M': /* host/address?query MD5 (128-bits) */
*b = '\0'; {
{ char digest[32 + 2];
char digest[32 + 2]; char BIGSTK buff[HTS_URLMAXSIZE * 2];
char BIGSTK buff[HTS_URLMAXSIZE * 2];
digest[0] = buff[0] = '\0'; digest[0] = buff[0] = '\0';
strcpybuff(buff, adr); strcpybuff(buff, adr);
strcatbuff(buff, fil_complete); strcatbuff(buff, fil_complete);
domd5mem(buff, strlen(buff), digest, 1); domd5mem(buff, strlen(buff), digest, 1);
strcpybuff(b, digest); htsbuff_cat(&sb, digest);
} } break;
b += strlen(b); // pointer à la fin
break;
case 'Q': case 'Q':
case 'q': /* query MD5 (128-bits/16-bits) case 'q': /* query MD5 (128-bits/16-bits)
GENERATED ONLY IF query string exists! */ GENERATED ONLY IF query string exists! */
{ {
char md5[32 + 2]; char md5[32 + 2];
*b = '\0'; htsbuff_catn(&sb, url_md5(md5, fil_complete), (tok == 'Q') ? 32 : 4);
strncatbuff(b, url_md5(md5, fil_complete), (tok == 'Q') ? 32 : 4); } break;
b += strlen(b); // pointer à la fin
}
break;
case 'r': case 'r':
case 'R': // protocol case 'R': // protocol
*b = '\0'; htsbuff_cat(&sb, protocol_str[protocol]);
strcatbuff(b, protocol_str[protocol]);
b += strlen(b); // pointer à la fin
break; break;
/* Patch by Juan Fco Rodriguez to get the full query string */ /* Patch by Juan Fco Rodriguez to get the full query string */
@@ -1021,19 +987,17 @@ int url_savename(lien_adrfilsave *const afs,
char *d = strchr(fil_complete, '?'); char *d = strchr(fil_complete, '?');
if (d != NULL) { if (d != NULL) {
strcatbuff(b, d); htsbuff_cat(&sb, d);
b += strlen(b);
} }
} }
break; break;
} }
} else } else
*b++ = *a++; htsbuff_catc(&sb, *a++);
} }
*b++ = '\0';
// //
// Types prédéfinis // predefined types
// //
} }

View File

@@ -351,6 +351,13 @@ static HTS_INLINE HTS_UNUSED void htsbuff_cat(htsbuff *b, const char *s) {
htsbuff_catn(b, s, (size_t) -1); htsbuff_catn(b, s, (size_t) -1);
} }
/** Append a single character (including '\0' as data). Aborts on overflow. */
static HTS_INLINE HTS_UNUSED void htsbuff_catc(htsbuff *b, char c) {
assertf__(1 < b->cap - b->len, "htsbuff append overflow", __FILE__, __LINE__);
b->buf[b->len++] = c;
b->buf[b->len] = '\0';
}
/** Reset content to s. Aborts on overflow. */ /** Reset content to s. Aborts on overflow. */
static HTS_INLINE HTS_UNUSED void htsbuff_cpy(htsbuff *b, const char *s) { static HTS_INLINE HTS_UNUSED void htsbuff_cpy(htsbuff *b, const char *s) {
b->len = 0; b->len = 0;

View File

@@ -193,6 +193,7 @@ HTSEXT_API int structcheck(const char *path);
HTSEXT_API int structcheck_utf8(const char *path); HTSEXT_API int structcheck_utf8(const char *path);
HTSEXT_API int dir_exists(const char *path); HTSEXT_API int dir_exists(const char *path);
HTSEXT_API void infostatuscode(char *msg, int statuscode); HTSEXT_API void infostatuscode(char *msg, int statuscode);
HTSEXT_API const char *infostatuscode_const(int statuscode);
HTSEXT_API TStamp mtime_local(void); HTSEXT_API TStamp mtime_local(void);
HTSEXT_API void qsec2str(char *st, TStamp t); HTSEXT_API void qsec2str(char *st, TStamp t);
HTSEXT_API char *int2char(strc_int2bytes2 * strc, int n); HTSEXT_API char *int2char(strc_int2bytes2 * strc, int n);