snac2

Fork of https://codeberg.org/grunfink/snac2
git clone https://git.inz.fi/snac2
Log | Files | Refs | README | LICENSE

commit 88b9a3187c6f41b8a752a13c04478bf6b4ba0593
parent d5b86e4e68c1121d0375544a4c389a88075cce30
Author: Santtu Lakkala <santtu.lakkala@unikie.com>
Date:   Fri, 21 Feb 2025 13:52:00 +0200

Add fmt helpers

Diffstat:
Mactivitypub.c | 43++++++++++++++-----------------------------
Mdata.c | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mformat.c | 37++++++++++++-------------------------
Mhtml.c | 38+++++++++++++++++++++++++++-----------
Mhttp.c | 22++++++++++++++++++++++
Mhttp_codes.h | 1+
Mhttpd.c | 2+-
Msnac.h | 5++++-
Mxs.h | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
9 files changed, 221 insertions(+), 84 deletions(-)

diff --git a/activitypub.c b/activitypub.c @@ -883,19 +883,15 @@ xs_str *process_tags(snac *snac, const char *content, xs_list **tag) if (valid_status(status) && actor && uid) { xs *d = xs_dict_new(); - xs *n = xs_fmt("@%s", uid); d = xs_dict_append(d, "type", "Mention"); d = xs_dict_append(d, "href", actor); - d = xs_dict_append(d, "name", n); + d = xs_dict_set_fmt(d, "name", "@%s", uid); tl = xs_list_append(tl, d); - link = xs_fmt("<span class=\"h-card\"><a href=\"%s\" class=\"u-url mention\">%s</a></span>", actor, n); + nc = xs_str_cat_fmt(nc, "<span class=\"h-card\"><a href=\"%s\" class=\"u-url mention\">@%s</a></span>", actor, uid); } - - if (!xs_is_null(link)) - nc = xs_str_cat(nc, link); else nc = xs_str_cat(nc, v); } @@ -904,17 +900,15 @@ xs_str *process_tags(snac *snac, const char *content, xs_list **tag) /* hashtag */ xs *d = xs_dict_new(); xs *n = xs_utf8_to_lower(v); - xs *h = xs_fmt("%s?t=%s", srv_baseurl, n + 1); - xs *l = xs_fmt("<a href=\"%s\" class=\"mention hashtag\" rel=\"tag\">%s</a>", h, v); d = xs_dict_append(d, "type", "Hashtag"); - d = xs_dict_append(d, "href", h); + d = xs_dict_set_fmt(d, "href", "%s?t=%s", srv_baseurl, n + 1); d = xs_dict_append(d, "name", n); tl = xs_list_append(tl, d); /* add the code */ - nc = xs_str_cat(nc, l); + nc = xs_str_cat_fmt(nc, "<a href=\"%s?t=%s\" class=\"mention hashtag\" rel=\"tag\">%s</a>", srv_baseurl, n + 1, v); } else if (*v == '&') { @@ -974,8 +968,8 @@ void notify(snac *snac, const char *type, const char *utype, const char *actor, return; /* if it's an announce by our own relay, done */ - xs *relay_id = xs_fmt("%s/relay", srv_baseurl); - if (xs_startswith(id, relay_id)) + if (xs_startswith(id, srv_baseurl) && + xs_startswith(id + strlen(srv_baseurl), "/relay")) return; } @@ -1008,22 +1002,16 @@ void notify(snac *snac, const char *type, const char *utype, const char *actor, ); if (strcmp(utype, "(null)") != 0) { - xs *s1 = xs_fmt("Type : %s + %s\n", type, utype); - body = xs_str_cat(body, s1); + body = xs_str_cat_fmt(body, "Type : %s + %s\n", type, utype); } else { - xs *s1 = xs_fmt("Type : %s\n", type); - body = xs_str_cat(body, s1); + body = xs_str_cat_fmt(body, "Type : %s\n", type); } - { - xs *s1 = xs_fmt("Actor : %s\n", actor); - body = xs_str_cat(body, s1); - } + body = xs_str_cat_fmt(body, "Actor: %s\n", actor); if (objid != NULL) { - xs *s1 = xs_fmt("Object: %s\n", objid); - body = xs_str_cat(body, s1); + body = xs_str_cat_fmt(body, "Object: %s\n", objid); } /* email */ @@ -1343,20 +1331,17 @@ xs_dict *msg_actor(snac *snac) char *folders[] = { "inbox", "outbox", "followers", "following", "featured", NULL }; for (n = 0; folders[n]; n++) { - xs *f = xs_fmt("%s/%s", snac->actor, folders[n]); - msg = xs_dict_set(msg, folders[n], f); + msg = xs_dict_set_fmt(msg, folders[n], "%s/%s", snac->actor, folders[n]); } p = xs_dict_get(snac->config, "avatar"); if (*p == '\0') - avtr = xs_fmt("%s/susie.png", srv_baseurl); - else - avtr = xs_dup(p); + p = avtr = xs_fmt("%s/susie.png", srv_baseurl); icon = xs_dict_append(icon, "type", "Image"); - icon = xs_dict_append(icon, "mediaType", xs_mime_by_ext(avtr)); - icon = xs_dict_append(icon, "url", avtr); + icon = xs_dict_append(icon, "mediaType", xs_mime_by_ext(p)); + icon = xs_dict_append(icon, "url", p); msg = xs_dict_set(msg, "icon", icon); kid = xs_fmt("%s#main-key", snac->actor); diff --git a/data.c b/data.c @@ -2542,6 +2542,68 @@ static int _load_raw_file(const char *fn, xs_val **data, int *size, return status; } +static int _load_raw_file_partial(const char *fn, xs_val **data, int *size, + const char *inm, xs_str **etag, int start, int *end) +/* loads a cached file */ +{ + int status = HTTP_STATUS_NOT_FOUND; + + if (fn) { + double tm = mtime(fn); + + if (tm > 0.0) { + /* file exists; build the etag */ + xs *e = xs_fmt("W/\"snac-%.0lf\"", tm); + + /* if if-none-match is set, check if it's the same */ + if (!xs_is_null(inm) && strcmp(e, inm) == 0) { + /* client has the newest version */ + status = HTTP_STATUS_NOT_MODIFIED; + } + else { + /* newer or never downloaded; read the full file */ + FILE *f; + + if ((f = fopen(fn, "rb")) != NULL) { + struct stat st; + int n; + if (fstat(fileno(f), &st) == 0) { + *size = st.st_size; + if (*end == XS_ALL) + *end = *size - 1; + } else { + *size = XS_ALL; + } + + n = *end == XS_ALL ? XS_ALL : *end - start + 1; + + if (*end != XS_ALL && *end > *size) + status = HTTP_STATUS_RANGE_NOT_SATISFIABLE; + else + if (start > *size || fseek(f, start, SEEK_SET) != 0) + status = HTTP_STATUS_RANGE_NOT_SATISFIABLE; + else + *data = xs_read(f, &n); + + *end = (n + start - 1); + fclose(f); + + if (status == HTTP_STATUS_NOT_FOUND) + status = HTTP_STATUS_PARTIAL_CONTENT; + } + } + + /* if caller wants the etag, return it */ + if (etag != NULL) + *etag = xs_dup(e); + + srv_debug(1, xs_fmt("_load_raw_file(): %s %d", fn, status)); + } + } + + return status; +} + xs_str *_static_fn(snac *snac, const char *id) /* gets the filename for a static file */ @@ -2562,6 +2624,15 @@ int static_get(snac *snac, const char *id, xs_val **data, int *size, return _load_raw_file(fn, data, size, inm, etag); } +int static_get_partial(snac *snac, const char *id, xs_val **data, int *size, + const char *inm, xs_str **etag, int start, int *end) +/* returns static content */ +{ + xs *fn = _static_fn(snac, id); + + return _load_raw_file_partial(fn, data, size, inm, etag, start, end); +} + void static_put(snac *snac, const char *id, const char *data, int size) /* writes status content */ diff --git a/format.c b/format.c @@ -110,41 +110,35 @@ static xs_str *format_line(const char *line, xs_list **attach) if (xs_startswith(v, "`")) { xs *s1 = xs_strip_chars_i(xs_dup(v), "`"); xs *e1 = encode_html(s1); - xs *s2 = xs_fmt("<code>%s</code>", e1); - s = xs_str_cat(s, s2); + s = xs_str_cat_fmt(s, "<code>%s</code>", e1); } else if (xs_startswith(v, "***")) { xs *s1 = xs_strip_chars_i(xs_dup(v), "*"); - xs *s2 = xs_fmt("<b><i>%s</i></b>", s1); - s = xs_str_cat(s, s2); + s = xs_str_cat_fmt(s, "<b><i>%s</i></b>", s1); } else if (xs_startswith(v, "**")) { xs *s1 = xs_strip_chars_i(xs_dup(v), "*"); - xs *s2 = xs_fmt("<b>%s</b>", s1); - s = xs_str_cat(s, s2); + s = xs_str_cat_fmt(s, "<b>%s</b>", s1); } else if (xs_startswith(v, "*")) { xs *s1 = xs_strip_chars_i(xs_dup(v), "*"); - xs *s2 = xs_fmt("<i>%s</i>", s1); - s = xs_str_cat(s, s2); + s = xs_str_cat_fmt(s, "<i>%s</i>", s1); } //anzu - begin else if (xs_startswith(v, "__")) { xs *s1 = xs_strip_chars_i(xs_dup(v), "_"); - xs *s2 = xs_fmt("<u>%s</u>", s1); - s = xs_str_cat(s, s2); + s = xs_str_cat_fmt(s, "<u>%s</u>", s1); } //anzu - end else if (xs_startswith(v, "~~")) { xs *s1 = xs_strip_chars_i(xs_dup(v), "~"); xs *e1 = encode_html(s1); - xs *s2 = xs_fmt("<s>%s</s>", e1); - s = xs_str_cat(s, s2); + s = xs_str_cat_fmt(s, "<s>%s</s>", e1); } else if (*v == '[') { @@ -159,9 +153,8 @@ static xs_str *format_line(const char *line, xs_list **attach) name = xs_crop_i(name, 1, 0); url = xs_crop_i(url, 0, -1); - xs *link = xs_fmt("<a href=\"%s\">%s</a>", url, name); - - s = xs_str_cat(s, link); + s = xs_str_cat_fmt(s, "<a href=\"%s\">%s</a>", + url, name); } else s = xs_str_cat(s, v); @@ -204,9 +197,7 @@ static xs_str *format_line(const char *line, xs_list **attach) } } else { - xs *link = xs_fmt("<a href=\"%s\">%s</a>", img_url, alt_text); - - s = xs_str_cat(s, link); + s = xs_str_cat_fmt(s, "<a href=\"%s\">%s</a>", img_url, alt_text); } } else @@ -245,8 +236,7 @@ static xs_str *format_line(const char *line, xs_list **attach) } } else { - xs *s1 = xs_fmt("<a href=\"%s\" target=\"_blank\">%s</a>", v2, u); - s = xs_str_cat(s, s1); + s = xs_str_cat_fmt(s, "<a href=\"%s\" target=\"_blank\">%s</a>", v2, u); } } else @@ -255,8 +245,7 @@ static xs_str *format_line(const char *line, xs_list **attach) xs *v2 = xs_strip_chars_i(xs_dup(u), ".,)"); - xs *s1 = xs_fmt("<a href=\"%s\" target=\"_blank\">%s</a>", v2, u); - s = xs_str_cat(s, s1); + s = xs_str_cat_fmt("<a href=\"%s\" target=\"_blank\">%s</a>", v2, u); } else s = xs_str_cat(s, v); @@ -462,10 +451,8 @@ xs_str *sanitize(const char *content) xs *el = xs_regex_select(v, "(src|href|rel|class|target)=(\"[^\"]*\"|'[^']*')"); xs *s3 = xs_join(el, " "); - s2 = xs_fmt("<%s%s%s%s>", + s = xs_str_cat_fmt(s, "<%s%s%s%s>", v[1] == '/' ? "/" : "", tag, xs_list_len(el) ? " " : "", s3); - - s = xs_str_cat(s, s2); } else { /* treat end of divs as paragraph breaks */ if (strcmp(v, "</div>")) diff --git a/html.c b/html.c @@ -1312,22 +1312,18 @@ xs_html *html_top_controls(snac *user) if (xs_type(md) == XSTYPE_DICT) { const xs_str *k; const xs_str *v; + const char *s = ""; metadata = xs_str_new(NULL); xs_dict_foreach(md, k, v) { - xs *kp = xs_fmt("%s=%s", k, v); - - if (*metadata) - metadata = xs_str_cat(metadata, "\n"); - metadata = xs_str_cat(metadata, kp); + metadata = xs_str_cat_fmt(metadata, "%s%s=%s", s, k, v); + s = "\n"; } } else if (xs_type(md) == XSTYPE_STRING) metadata = xs_dup(md); - else - metadata = xs_str_new(NULL); /* ui language */ xs_html *lang_select = xs_html_tag("select", @@ -1568,7 +1564,7 @@ xs_html *html_top_controls(snac *user) xs_html_attr("rows", "4"), xs_html_attr("placeholder", "Blog=https:/" "/example.com/my-blog\nGPG Key=1FA54\n..."), - xs_html_text(metadata))), + xs_html_text(metadata ? metadata : ""))), xs_html_tag("p", xs_html_text(L("Web interface language:")), @@ -3604,7 +3600,8 @@ void set_user_lang(snac *user) int html_get_handler(const xs_dict *req, const char *q_path, char **body, int *b_size, char **ctype, - xs_str **etag, xs_str **last_modified) + xs_str **etag, xs_str **last_modified, + xs_dict **headers) { const char *accept = xs_dict_get(req, "accept"); int status = HTTP_STATUS_NOT_FOUND; @@ -4111,10 +4108,29 @@ int html_get_handler(const xs_dict *req, const char *q_path, int sz; if (id && *id) { - status = static_get(&snac, id, body, &sz, + int start; + int end; + + if (parse_range(req, &start, &end)) { + status = static_get_partial(&snac, id, body, &sz, + xs_dict_get(req, "if-none-match"), etag, + start, &end); + if (status == HTTP_STATUS_PARTIAL_CONTENT) { + xs *part = NULL; + if (sz != XS_ALL) + part = xs_fmt("bytes %d-%d/%d", start, end, sz); + else + part = xs_fmt("bytes %d-%d/*", start, end); + *headers = xs_dict_append(*headers, "Content-Range", part); + *b_size = end - start + 1; + *ctype = (char *)xs_mime_by_ext(id); + } + } else { + status = static_get(&snac, id, body, &sz, xs_dict_get(req, "if-none-match"), etag); + } - if (valid_status(status)) { + if (valid_status(status) && status != HTTP_STATUS_PARTIAL_CONTENT) { *b_size = sz; *ctype = (char *)xs_mime_by_ext(id); } diff --git a/http.c b/http.c @@ -260,3 +260,25 @@ int check_signature(const xs_dict *req, xs_str **err) return 1; } + +int parse_range(const xs_dict *req, int *start, int *end) +{ + const char *rng = xs_dict_get(req, "range"); + if (rng && xs_str_in(rng, ",") == -1 && xs_startswith(rng, "bytes=")) { + xs *l = xs_split_n(rng + strlen("bytes="), "-", 2); + if (xs_list_len(l) == 2) { + const char *ss = xs_list_get(l, 0); + const char *es = xs_list_get(l, 1); + + if (ss[strspn(ss, "0123456789")] == '\0' && + es[strspn(es, "0123456789")] == '\0') { + *start = atoi(ss); + *end = es[0] == '\0' ? XS_ALL : atoi(es); + + return 1; + } + } + } + + return 0; +} diff --git a/http_codes.h b/http_codes.h @@ -33,6 +33,7 @@ HTTP_STATUS(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) HTTP_STATUS(408, REQUEST_TIMEOUT, Request Timeout) HTTP_STATUS(409, CONFLICT, Conflict) HTTP_STATUS(410, GONE, Gone) +HTTP_STATUS(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) HTTP_STATUS(421, MISDIRECTED_REQUEST, Misdirected Request) HTTP_STATUS(422, UNPROCESSABLE_CONTENT, Unprocessable Content) HTTP_STATUS(499, CLIENT_CLOSED_REQUEST, Client Closed Request) diff --git a/httpd.c b/httpd.c @@ -440,7 +440,7 @@ void httpd_connection(FILE *f) #endif /* NO_MASTODON_API */ if (status == 0) - status = html_get_handler(req, q_path, &body, &b_size, &ctype, &etag, &last_modified); + status = html_get_handler(req, q_path, &body, &b_size, &ctype, &etag, &last_modified, &headers); } else if (strcmp(method, "POST") == 0) { diff --git a/snac.h b/snac.h @@ -239,6 +239,7 @@ int actor_get(const char *actor, xs_dict **data); int actor_get_refresh(snac *user, const char *actor, xs_dict **data); int static_get(snac *snac, const char *id, xs_val **data, int *size, const char *inm, xs_str **etag); +int static_get_partial(snac *snac, const char *id, xs_val **data, int *size, const char *inm, xs_str **etag, int start, int *end); void static_put(snac *snac, const char *id, const char *data, int size); void static_put_meta(snac *snac, const char *id, const char *str); xs_str *static_get_meta(snac *snac, const char *id); @@ -318,6 +319,7 @@ xs_dict *http_signed_request(snac *snac, const char *method, const char *url, int *status, xs_str **payload, int *p_size, int timeout); int check_signature(const xs_dict *req, xs_str **err); +int parse_range(const xs_dict *req, int *start, int *end); srv_state *srv_state_op(xs_str **fname, int op); void httpd(void); @@ -392,7 +394,8 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, int html_get_handler(const xs_dict *req, const char *q_path, char **body, int *b_size, char **ctype, - xs_str **etag, xs_str **last_modified); + xs_str **etag, xs_str **last_modified, + xs_dict **headers); int html_post_handler(const xs_dict *req, const char *q_path, char *payload, int p_size, diff --git a/xs.h b/xs.h @@ -75,6 +75,7 @@ xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix); #define xs_str_prepend_i(str, prefix) xs_str_wrap_i(prefix, str, NULL) xs_str *_xs_str_cat(xs_str *str, const char *strs[]); #define xs_str_cat(str, ...) _xs_str_cat(str, (const char *[]){ __VA_ARGS__, NULL }) +xs_str *xs_str_cat_fmt(xs_str *str, const char *fmt, ...); xs_str *xs_replace_in(xs_str *str, const char *sfrom, const char *sto, int times); #define xs_replace_i(str, sfrom, sto) xs_replace_in(str, sfrom, sto, XS_ALL) #define xs_replace(str, sfrom, sto) xs_replace_in(xs_dup(str), sfrom, sto, XS_ALL) @@ -124,6 +125,7 @@ const xs_val *xs_dict_get(const xs_dict *dict, const xs_str *key); #define xs_dict_get_def(dict, key, def) xs_or(xs_dict_get(dict, key), def) xs_dict *xs_dict_del(xs_dict *dict, const xs_str *key); xs_dict *xs_dict_set(xs_dict *dict, const xs_str *key, const xs_val *data); +xs_dict *xs_dict_set_fmt(xs_dict *dict, const xs_str *key, const char *fmt, ...); xs_dict *xs_dict_gc(const xs_dict *dict); const xs_val *xs_dict_get_path_sep(const xs_dict *dict, const char *path, const char *sep); @@ -517,6 +519,26 @@ xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix) return str; } +xs_str *xs_str_cat_fmt(xs_str *str, const char *fmt, ...) +{ + int o = strlen(str); + int n; + xs_str *s = NULL; + va_list ap; + + va_start(ap, fmt); + n = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + + if (n++ > 0) { + str = xs_realloc(str, o + n); + va_start(ap, fmt); + vsnprintf(str + o, n, fmt, ap); + va_end(ap); + } + + return str; +} xs_str *_xs_str_cat(xs_str *str, const char *strs[]) /* concatenates all strings after str */ @@ -1119,13 +1141,8 @@ static int *_xs_dict_locate(const xs_dict *dict, const char *key) return off; } - -xs_dict *xs_dict_set(xs_dict *dict, const xs_str *key, const xs_val *value) -/* sets a key/value pair */ +xs_dict *_xs_dict_ensure(xs_dict *dict, const xs_str *key, int vsz, int *offset) { - if (value == NULL) - value = xs_stock(XSTYPE_NULL); - if (xs_type(dict) == XSTYPE_DICT) { int *o = _xs_dict_locate(dict, key); int end = xs_size(dict); @@ -1135,7 +1152,6 @@ xs_dict *xs_dict_set(xs_dict *dict, const xs_str *key, const xs_val *value) *o = end; int ksz = xs_size(key); - int vsz = xs_size(value); int dsz = sizeof(ditem_hdr) + ksz + vsz; /* open room in the dict for the full ditem */ @@ -1153,9 +1169,6 @@ xs_dict *xs_dict_set(xs_dict *dict, const xs_str *key, const xs_val *value) /* copy the key */ memcpy(di->key, key, ksz); - /* copy the value */ - memcpy(dict + di->value_offset, value, vsz); - /* chain to the sequential list */ if (dh->first == 0) dh->first = end; @@ -1166,6 +1179,8 @@ xs_dict *xs_dict_set(xs_dict *dict, const xs_str *key, const xs_val *value) } dh->last = end; + + *offset = di->value_offset; } else { /* ditem already exists */ @@ -1182,20 +1197,57 @@ xs_dict *xs_dict_set(xs_dict *dict, const xs_str *key, const xs_val *value) xs_val *o_value = dict + *i; /* will new value fit over the old one? */ - if (xs_size(value) <= xs_size(o_value)) { - /* just overwrite */ - /* (difference is leaked inside the dict) */ - memcpy(o_value, value, xs_size(value)); - } - else { + if (vsz > xs_size(o_value)) { /* not enough room: new value will live at the end of the dict */ /* (old value is leaked inside the dict) */ *i = end; - dict = xs_insert(dict, end, value); + dict = xs_expand(dict, end, vsz); } + *offset = *i; } } + else { + *offset = -1; + } + + return dict; +} + +xs_dict *xs_dict_set_fmt(xs_dict *dict, const xs_str *key, const char *fmt, ...) +{ + if (xs_type(dict) == XSTYPE_DICT) { + int o; + int vsz; + va_list ap; + + va_start(ap, fmt); + vsz = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + + if (vsz++ < 0) + return dict; + + dict = _xs_dict_ensure(dict, key, vsz, &o); + + va_start(ap, fmt); + vsnprintf(dict + o, vsz, fmt, ap); + va_end(ap); + } +} + +xs_dict *xs_dict_set(xs_dict *dict, const xs_str *key, const xs_val *value) +/* sets a key/value pair */ +{ + if (value == NULL) + value = xs_stock(XSTYPE_NULL); + + if (xs_type(dict) == XSTYPE_DICT) { + int vsz = xs_size(value); + int o; + dict = _xs_dict_ensure(dict, key, vsz, &o); + memcpy(dict + o, value, vsz); + } return dict; }