Browse Source

Merge branch 'master' into master

ltning 1 year ago
parent
commit
114ed37f9c
20 changed files with 173 additions and 43 deletions
  1. 1 0
      README.md
  2. 16 0
      RELEASE_NOTES.md
  3. 4 4
      TODO.md
  4. 1 1
      activitypub.c
  5. 39 6
      data.c
  6. 5 0
      doc/snac.1
  7. 1 1
      doc/snac.5
  8. 1 0
      doc/style.css
  9. 36 14
      html.c
  10. 13 0
      main.c
  11. 1 1
      mastoapi.c
  12. 7 3
      snac.h
  13. 1 0
      utils.c
  14. 28 8
      xs.h
  15. 2 1
      xs_io.h
  16. 6 1
      xs_match.h
  17. 1 1
      xs_openssl.h
  18. 2 0
      xs_socket.h
  19. 7 1
      xs_url.h
  20. 1 1
      xs_version.h

+ 1 - 0
README.md

@@ -107,6 +107,7 @@ This will:
 - [How to install snac on OpenBSD without relayd (by @antics@mastodon.nu)](https://chai.guru/pub/openbsd/snac.html).
 - [How to install snac on OpenBSD without relayd (by @antics@mastodon.nu)](https://chai.guru/pub/openbsd/snac.html).
 - [Setting up Snac in OpenBSD (by Yonle)](https://wiki.ircnow.org/index.php?n=Openbsd.Snac).
 - [Setting up Snac in OpenBSD (by Yonle)](https://wiki.ircnow.org/index.php?n=Openbsd.Snac).
 - [How to run your own social network with snac (by Giacomo Tesio)](https://encrypted.tesio.it/2024/12/18/how-to-run-your-own-social-network.html). Includes information on how to run snac as a CGI.
 - [How to run your own social network with snac (by Giacomo Tesio)](https://encrypted.tesio.it/2024/12/18/how-to-run-your-own-social-network.html). Includes information on how to run snac as a CGI.
+- [Improving snac Performance with Nginx Proxy Cache (by Stefano Marinelli)](https://it-notes.dragas.net/2025/01/29/improving-snac-performance-with-nginx-proxy-cache/).
 
 
 ## Incredibly awesome CSS themes for snac
 ## Incredibly awesome CSS themes for snac
 
 

+ 16 - 0
RELEASE_NOTES.md

@@ -1,5 +1,21 @@
 # Release Notes
 # Release Notes
 
 
+## 2.71
+
+Fixed memory leak (contributed by inz).
+
+Fixed crash.
+
+## 2.70
+
+Notifications are now shown in a more compact way (i.e. all reactions are shown just above your post, instead of repeating the post *ad nauseam* for every reaction).
+
+New command-line option `unmute` to, well, no-longer-mute an actor.
+
+The private timeline now includes an approximate mark between new posts and "already seen" ones.
+
+Fixed a spurious 404 error in the instance root URL for some configurations.
+
 ## 2.69 "Yin/Yang of Love"
 ## 2.69 "Yin/Yang of Love"
 
 
 Added support for subscribing to LitePub (Pleroma-style) Fediverse Relays like e.g. https://fedi-relay.gyptazy.com to improve federation. See `snac(8)` (the Administrator Manual) for more information on how to use this feature.
 Added support for subscribing to LitePub (Pleroma-style) Fediverse Relays like e.g. https://fedi-relay.gyptazy.com to improve federation. See `snac(8)` (the Administrator Manual) for more information on how to use this feature.

+ 4 - 4
TODO.md

@@ -14,14 +14,10 @@ Important: deleting a follower should do more that just delete the object, see h
 
 
 ## Wishlist
 ## Wishlist
 
 
-Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information).
-
 The instance timeline should also show boosts from users.
 The instance timeline should also show boosts from users.
 
 
 Mastoapi: implement /v1/conversations.
 Mastoapi: implement /v1/conversations.
 
 
-Implement following of hashtags (this is not trivial).
-
 Track 'Event' data types standardization; how to add plan-to-attend and similar activities (more info: https://event-federation.eu/). Friendica interacts with events via activities `Accept` (will go), `TentativeAccept` (will try to go) or `Reject` (cannot go) (`object` field as id, not object). `Undo` for any of these activities cancel (`object` as an object, not id).
 Track 'Event' data types standardization; how to add plan-to-attend and similar activities (more info: https://event-federation.eu/). Friendica interacts with events via activities `Accept` (will go), `TentativeAccept` (will try to go) or `Reject` (cannot go) (`object` field as id, not object). `Undo` for any of these activities cancel (`object` as an object, not id).
 
 
 Implement "FEP-3b86: Activity Intents" https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md
 Implement "FEP-3b86: Activity Intents" https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md
@@ -365,3 +361,7 @@ CSV import/export does not work with OpenBSD security on; document it or fix it
 Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details) (2025-01-06T18:43:52+0100).
 Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details) (2025-01-06T18:43:52+0100).
 
 
 Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+0100).
 Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+0100).
+
+Implement following of hashtags (this is not trivial) (2025-01-30T16:12:16+0100).
+
+Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information) (2025-01-30T16:12:34+0100).

+ 1 - 1
activitypub.c

@@ -3081,7 +3081,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
         int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
         int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
 
 
         /* get the public outbox or the pinned list */
         /* get the public outbox or the pinned list */
-        xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt) : pinned_list(&snac);
+        xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt, NULL) : pinned_list(&snac);
 
 
         xs_list_foreach(elems, v) {
         xs_list_foreach(elems, v) {
             xs *i = NULL;
             xs *i = NULL;

+ 39 - 6
data.c

@@ -1489,16 +1489,28 @@ xs_str *user_index_fn(snac *user, const char *idx_name)
 }
 }
 
 
 
 
-xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show)
+xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more)
 /* returns a timeline (with all entries) */
 /* returns a timeline (with all entries) */
 {
 {
     xs *idx = user_index_fn(user, idx_name);
     xs *idx = user_index_fn(user, idx_name);
 
 
-    return index_list_desc(idx, skip, show);
+    /* if a more flag is sent, request one more */
+    xs_list *lst = index_list_desc(idx, skip, show + (more != NULL ? 1 : 0));
+
+    if (more != NULL) {
+        if (xs_list_len(lst) > show) {
+            *more = 1;
+            lst = xs_list_del(lst, -1);
+        }
+        else
+            *more = 0;
+    }
+
+    return lst;
 }
 }
 
 
 
 
-xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show)
+xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more)
 /* returns a timeline (only top level entries) */
 /* returns a timeline (only top level entries) */
 {
 {
     int c_max;
     int c_max;
@@ -1510,12 +1522,33 @@ xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show)
     if (show > c_max)
     if (show > c_max)
         show = c_max;
         show = c_max;
 
 
-    xs *list = timeline_simple_list(snac, idx_name, skip, show);
+    xs *list = timeline_simple_list(snac, idx_name, skip, show, more);
 
 
     return timeline_top_level(snac, list);
     return timeline_top_level(snac, list);
 }
 }
 
 
 
 
+void timeline_add_mark(snac *user)
+/* adds an "already seen" mark to the private timeline */
+{
+    xs *fn = xs_fmt("%s/private.idx", user->basedir);
+    char last_entry[MD5_HEX_SIZE] = "";
+    FILE *f;
+
+    /* get the last entry in the index */
+    if ((f = fopen(fn, "r")) != NULL) {
+        index_desc_first(f, last_entry, 0);
+        fclose(f);
+    }
+
+    /* is the last entry *not* a mark? */
+    if (strcmp(last_entry, MD5_ALREADY_SEEN_MARK) != 0) {
+        /* add it */
+        index_add_md5(fn, MD5_ALREADY_SEEN_MARK);
+    }
+}
+
+
 xs_str *instance_index_fn(void)
 xs_str *instance_index_fn(void)
 {
 {
     return xs_fmt("%s/public.idx", srv_basedir);
     return xs_fmt("%s/public.idx", srv_basedir);
@@ -2709,9 +2742,9 @@ xs_list *content_search(snac *user, const char *regex,
     const char *md5s[3] = {0};
     const char *md5s[3] = {0};
     int c[3] = {0};
     int c[3] = {0};
 
 
-    tls[0] = timeline_simple_list(user, "public", 0, XS_ALL);   /* public */
+    tls[0] = timeline_simple_list(user, "public", 0, XS_ALL, NULL);   /* public */
     tls[1] = timeline_instance_list(0, XS_ALL); /* instance */
     tls[1] = timeline_instance_list(0, XS_ALL); /* instance */
-    tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL) : xs_list_new(); /* private or none */
+    tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL, NULL) : xs_list_new(); /* private or none */
 
 
     /* first positioning */
     /* first positioning */
     for (int n = 0; n < 3; n++)
     for (int n = 0; n < 3; n++)

+ 5 - 0
doc/snac.1

@@ -234,6 +234,8 @@ Purges old data from the timeline of all users.
 .It Cm adduser Ar basedir Op uid
 .It Cm adduser Ar basedir Op uid
 Adds a new user to the server. This is an interactive command;
 Adds a new user to the server. This is an interactive command;
 necessary information will be prompted for.
 necessary information will be prompted for.
+.It Cm deluser Ar basedir Ar uid
+Deletes a user, unfollowing all accounts first.
 .It Cm resetpwd Ar basedir Ar uid
 .It Cm resetpwd Ar basedir Ar uid
 Resets a user's password to a new, random one.
 Resets a user's password to a new, random one.
 .It Cm queue Ar basedir Ar uid
 .It Cm queue Ar basedir Ar uid
@@ -257,6 +259,9 @@ The rest of command line arguments are treated as media files to be
 attached to the post.
 attached to the post.
 .It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ...
 .It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ...
 Like the previous one, but creates an "unlisted" (or "quiet public") post.
 Like the previous one, but creates an "unlisted" (or "quiet public") post.
+.It Cm note_mention Ar basedir Ar uid Ar text Op file file ...
+Like the previous one, but creates a post only for accounts mentioned
+in the post body.
 .It Cm block Ar basedir Ar instance_url
 .It Cm block Ar basedir Ar instance_url
 Blocks a full instance, given its URL or domain name. All subsequent
 Blocks a full instance, given its URL or domain name. All subsequent
 incoming activities with identifiers from that instance will be immediately
 incoming activities with identifiers from that instance will be immediately

+ 1 - 1
doc/snac.5

@@ -78,7 +78,7 @@ converted to related emojis:
 .Ss Accepted HTML
 .Ss Accepted HTML
 All HTML tags in entries are neutered except the following ones:
 All HTML tags in entries are neutered except the following ones:
 .Bd -literal
 .Bd -literal
-a p br blockquote ul ol li cite small
+a p br blockquote ul ol li cite small h2 h3
 span i b u s pre code em strong hr img del
 span i b u s pre code em strong hr img del
 .Ed
 .Ed
 .Pp
 .Pp

+ 1 - 0
doc/style.css

@@ -29,6 +29,7 @@ pre { overflow-x: scroll; }
 .snac-list-of-lists { padding-left: 0; }
 .snac-list-of-lists { padding-left: 0; }
 .snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px;
 .snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px;
   margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; }
   margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; }
+.snac-no-more-unseen-posts { border-top: 1px solid #a0a0a0; border-bottom: 1px solid #a0a0a0; padding: 0.5em 0; margin: 1em 0; }
 @media (prefers-color-scheme: dark) { 
 @media (prefers-color-scheme: dark) { 
   body, input, textarea { background-color: #000; color: #fff; }
   body, input, textarea { background-color: #000; color: #fff; }
   a { color: #7799dd }
   a { color: #7799dd }

+ 36 - 14
html.c

@@ -2658,10 +2658,32 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
     xs_html_add(body,
     xs_html_add(body,
         posts);
         posts);
 
 
+    int mark_shown = 0;
+
     while (xs_list_iter(&p, &v)) {
     while (xs_list_iter(&p, &v)) {
         xs *msg = NULL;
         xs *msg = NULL;
         int status;
         int status;
 
 
+        /* "already seen" mark? */
+        if (strcmp(v, MD5_ALREADY_SEEN_MARK) == 0) {
+            if (skip == 0 && !mark_shown) {
+                xs *s = xs_fmt("%s/admin", user->actor);
+
+                xs_html_add(posts,
+                    xs_html_tag("div",
+                        xs_html_attr("class", "snac-no-more-unseen-posts"),
+                        xs_html_text(L("No more unseen posts")),
+                        xs_html_text(" - "),
+                        xs_html_tag("a",
+                            xs_html_attr("href", s),
+                            xs_html_text(L("Back to top")))));
+            }
+
+            mark_shown = 1;
+
+            continue;
+        }
+
         if (utl && user && !is_pinned_by_md5(user, v))
         if (utl && user && !is_pinned_by_md5(user, v))
             status = timeline_get_by_md5(user, v, &msg);
             status = timeline_get_by_md5(user, v, &msg);
         else
         else
@@ -3324,21 +3346,17 @@ int html_get_handler(const xs_dict *req, const char *q_path,
         }
         }
         else {
         else {
             xs *list = NULL;
             xs *list = NULL;
-            xs *next = NULL;
+            int more = 0;
 
 
-            if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines"))) {
-                list = timeline_simple_list(&snac, "public", skip, show);
-                next = timeline_simple_list(&snac, "public", skip + show, 1);
-            }
-            else {
-                list = timeline_list(&snac, "public", skip, show);
-                next = timeline_list(&snac, "public", skip + show, 1);
-            }
+            if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")))
+                list = timeline_simple_list(&snac, "public", skip, show, &more);
+            else 
+                list = timeline_list(&snac, "public", skip, show, &more);
 
 
             xs *pins = pinned_list(&snac);
             xs *pins = pinned_list(&snac);
             pins = xs_list_cat(pins, list);
             pins = xs_list_cat(pins, list);
 
 
-            *body = html_timeline(&snac, pins, 1, skip, show, xs_list_len(next), NULL, "", 1, error);
+            *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error);
 
 
             *b_size = strlen(*body);
             *b_size = strlen(*body);
             status  = HTTP_STATUS_OK;
             status  = HTTP_STATUS_OK;
@@ -3487,6 +3505,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
                 }
                 }
             }
             }
             else {
             else {
+                /** the private timeline **/
                 double t = history_mtime(&snac, "timeline.html_");
                 double t = history_mtime(&snac, "timeline.html_");
 
 
                 /* if enabled by admin, return a cached page if its timestamp is:
                 /* if enabled by admin, return a cached page if its timestamp is:
@@ -3500,19 +3519,22 @@ int html_get_handler(const xs_dict *req, const char *q_path,
                                 xs_dict_get(req, "if-none-match"), etag);
                                 xs_dict_get(req, "if-none-match"), etag);
                 }
                 }
                 else {
                 else {
+                    int more = 0;
+
                     snac_debug(&snac, 1, xs_fmt("building timeline"));
                     snac_debug(&snac, 1, xs_fmt("building timeline"));
 
 
-                    xs *list = timeline_list(&snac, "private", skip, show);
-                    xs *next = timeline_list(&snac, "private", skip + show, 1);
+                    xs *list = timeline_list(&snac, "private", skip, show, &more);
 
 
                     *body = html_timeline(&snac, list, 0, skip, show,
                     *body = html_timeline(&snac, list, 0, skip, show,
-                            xs_list_len(next), NULL, "/admin", 1, error);
+                            more, NULL, "/admin", 1, error);
 
 
                     *b_size = strlen(*body);
                     *b_size = strlen(*body);
                     status  = HTTP_STATUS_OK;
                     status  = HTTP_STATUS_OK;
 
 
                     if (save)
                     if (save)
                         history_add(&snac, "timeline.html_", *body, *b_size, etag);
                         history_add(&snac, "timeline.html_", *body, *b_size, etag);
+
+                    timeline_add_mark(&snac);
                 }
                 }
             }
             }
         }
         }
@@ -3712,7 +3734,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 
 
         int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
         int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
 
 
-        xs *elems = timeline_simple_list(&snac, "public", 0, cnt);
+        xs *elems = timeline_simple_list(&snac, "public", 0, cnt, NULL);
         xs *bio   = xs_dup(xs_dict_get(snac.config, "bio"));
         xs *bio   = xs_dup(xs_dict_get(snac.config, "bio"));
 
 
         xs *rss_title = xs_fmt("%s (@%s@%s)",
         xs *rss_title = xs_fmt("%s (@%s@%s)",

+ 13 - 0
main.c

@@ -49,6 +49,7 @@ int usage(void)
     printf("unblock {basedir} {instance_url}     Unblocks a full instance\n");
     printf("unblock {basedir} {instance_url}     Unblocks a full instance\n");
     printf("limit {basedir} {uid} {actor}        Limits an actor (drops their announces)\n");
     printf("limit {basedir} {uid} {actor}        Limits an actor (drops their announces)\n");
     printf("unlimit {basedir} {uid} {actor}      Unlimits an actor\n");
     printf("unlimit {basedir} {uid} {actor}      Unlimits an actor\n");
+    printf("unmute {basedir} {uid} {actor}       Unmutes a previously muted actor\n");
     printf("verify_links {basedir} {uid}         Verifies a user's links (in the metadata)\n");
     printf("verify_links {basedir} {uid}         Verifies a user's links (in the metadata)\n");
     printf("search {basedir} {uid} {regex}       Searches posts by content\n");
     printf("search {basedir} {uid} {regex}       Searches posts by content\n");
     printf("export_csv {basedir} {uid}           Exports data as CSV files\n");
     printf("export_csv {basedir} {uid}           Exports data as CSV files\n");
@@ -446,6 +447,18 @@ int main(int argc, char *argv[])
         return 0;
         return 0;
     }
     }
 
 
+    if (strcmp(cmd, "unmute") == 0) { /** **/
+        if (is_muted(&snac, url)) {
+            unmute(&snac, url);
+
+            printf("%s unmuted\n", url);
+        }
+        else
+            printf("%s actor is not muted\n", url);
+
+        return 0;
+    }
+
     if (strcmp(cmd, "search") == 0) { /** **/
     if (strcmp(cmd, "search") == 0) { /** **/
         int to;
         int to;
 
 

+ 1 - 1
mastoapi.c

@@ -1676,7 +1676,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                 else
                 else
                 if (strcmp(opt, "statuses") == 0) { /** **/
                 if (strcmp(opt, "statuses") == 0) { /** **/
                     /* the public list of posts of a user */
                     /* the public list of posts of a user */
-                    xs *timeline = timeline_simple_list(&snac2, "public", 0, 256);
+                    xs *timeline = timeline_simple_list(&snac2, "public", 0, 256, NULL);
                     xs_list *p   = timeline;
                     xs_list *p   = timeline;
                     const xs_str *v;
                     const xs_str *v;
 
 

+ 7 - 3
snac.h

@@ -1,7 +1,7 @@
 /* snac - A simple, minimalistic ActivityPub instance */
 /* snac - A simple, minimalistic ActivityPub instance */
 /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
 /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
 
 
-#define VERSION "2.70-dev"
+#define VERSION "2.72-dev"
 
 
 #define USER_AGENT "snac/" VERSION
 #define USER_AGENT "snac/" VERSION
 
 
@@ -22,6 +22,8 @@
 
 
 #define MD5_HEX_SIZE 33
 #define MD5_HEX_SIZE 33
 
 
+#define MD5_ALREADY_SEEN_MARK "00000000000000000000000000000000"
+
 extern double disk_layout;
 extern double disk_layout;
 extern xs_str *srv_basedir;
 extern xs_str *srv_basedir;
 extern xs_dict *srv_config;
 extern xs_dict *srv_config;
@@ -157,12 +159,14 @@ int timeline_here(snac *snac, const char *md5);
 int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg);
 int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg);
 int timeline_del(snac *snac, const char *id);
 int timeline_del(snac *snac, const char *id);
 xs_str *user_index_fn(snac *user, const char *idx_name);
 xs_str *user_index_fn(snac *user, const char *idx_name);
-xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show);
-xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show);
+xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more);
+xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more);
 int timeline_add(snac *snac, const char *id, const xs_dict *o_msg);
 int timeline_add(snac *snac, const char *id, const xs_dict *o_msg);
 int timeline_admire(snac *snac, const char *id, const char *admirer, int like);
 int timeline_admire(snac *snac, const char *id, const char *admirer, int like);
 
 
 xs_list *timeline_top_level(snac *snac, const xs_list *list);
 xs_list *timeline_top_level(snac *snac, const xs_list *list);
+void timeline_add_mark(snac *user);
+
 xs_list *local_list(snac *snac, int max);
 xs_list *local_list(snac *snac, int max);
 xs_str *instance_index_fn(void);
 xs_str *instance_index_fn(void);
 xs_list *timeline_instance_list(int skip, int show);
 xs_list *timeline_instance_list(int skip, int show);

+ 1 - 0
utils.c

@@ -73,6 +73,7 @@ static const char *default_css =
     ".snac-list-of-lists { padding-left: 0; }\n"
     ".snac-list-of-lists { padding-left: 0; }\n"
     ".snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px;\n"
     ".snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px;\n"
     "  margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; }\n"
     "  margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; }\n"
+    ".snac-no-more-unseen-posts { border-top: 1px solid #a0a0a0; border-bottom: 1px solid #a0a0a0; padding: 0.5em 0; margin: 1em 0; }\n"
     "@media (prefers-color-scheme: dark) { \n"
     "@media (prefers-color-scheme: dark) { \n"
     "  body, input, textarea { background-color: #000; color: #fff; }\n"
     "  body, input, textarea { background-color: #000; color: #fff; }\n"
     "  a { color: #7799dd }\n"
     "  a { color: #7799dd }\n"

+ 28 - 8
xs.h

@@ -12,6 +12,7 @@
 #include <stdarg.h>
 #include <stdarg.h>
 #include <signal.h>
 #include <signal.h>
 #include <errno.h>
 #include <errno.h>
+#include <stdint.h>
 
 
 typedef enum {
 typedef enum {
     XSTYPE_STRING = 0x02,       /* C string (\0 delimited) (NOT STORED) */
     XSTYPE_STRING = 0x02,       /* C string (\0 delimited) (NOT STORED) */
@@ -142,6 +143,7 @@ void xs_data_get(void *data, const xs_data *value);
 void *xs_memmem(const char *haystack, int h_size, const char *needle, int n_size);
 void *xs_memmem(const char *haystack, int h_size, const char *needle, int n_size);
 
 
 unsigned int xs_hash_func(const char *data, int size);
 unsigned int xs_hash_func(const char *data, int size);
+uint64_t xs_hash64_func(const char *data, int size);
 
 
 #ifdef XS_ASSERT
 #ifdef XS_ASSERT
 #include <assert.h>
 #include <assert.h>
@@ -632,7 +634,7 @@ xs_str *xs_crop_i(xs_str *str, int start, int end)
         end = sz + end;
         end = sz + end;
 
 
     /* crop from the top */
     /* crop from the top */
-    if (end > 0 && end < sz)
+    if (end >= 0 && end < sz)
         str[end] = '\0';
         str[end] = '\0';
 
 
     /* crop from the bottom */
     /* crop from the bottom */
@@ -989,16 +991,20 @@ xs_str *xs_join(const xs_list *list, const char *sep)
 xs_list *xs_split_n(const char *str, const char *sep, int times)
 xs_list *xs_split_n(const char *str, const char *sep, int times)
 /* splits a string into a list upto n times */
 /* splits a string into a list upto n times */
 {
 {
+    xs_list *list = xs_list_new();
+
+    if (!xs_is_string(str) || !xs_is_string(sep))
+        return list;
+
     int sz = strlen(sep);
     int sz = strlen(sep);
     char *ss;
     char *ss;
-    xs_list *list;
-
-    list = xs_list_new();
 
 
     while (times > 0 && (ss = strstr(str, sep)) != NULL) {
     while (times > 0 && (ss = strstr(str, sep)) != NULL) {
         /* create a new string with this slice and add it to the list */
         /* create a new string with this slice and add it to the list */
         xs *s = xs_str_new_sz(str, ss - str);
         xs *s = xs_str_new_sz(str, ss - str);
-        list = xs_list_append(list, s);
+
+        if (xs_is_string(s))
+            list = xs_list_append(list, s);
 
 
         /* skip past the separator */
         /* skip past the separator */
         str = ss + sz;
         str = ss + sz;
@@ -1007,7 +1013,8 @@ xs_list *xs_split_n(const char *str, const char *sep, int times)
     }
     }
 
 
     /* add the rest of the string */
     /* add the rest of the string */
-    list = xs_list_append(list, str);
+    if (xs_is_string(str))
+        list = xs_list_append(list, str);
 
 
     return list;
     return list;
 }
 }
@@ -1487,9 +1494,8 @@ unsigned int xs_hash_func(const char *data, int size)
 /* a general purpose hashing function */
 /* a general purpose hashing function */
 {
 {
     unsigned int hash = 0x666;
     unsigned int hash = 0x666;
-    int n;
 
 
-    for (n = 0; n < size; n++) {
+    for (int n = 0; n < size; n++) {
         hash ^= (unsigned char)data[n];
         hash ^= (unsigned char)data[n];
         hash *= 111111111;
         hash *= 111111111;
     }
     }
@@ -1498,6 +1504,20 @@ unsigned int xs_hash_func(const char *data, int size)
 }
 }
 
 
 
 
+uint64_t xs_hash64_func(const char *data, int size)
+/* a general purpose hashing function (64 bit) */
+{
+    uint64_t hash = 0x100;
+
+    for (int n = 0; n < size; n++) {
+        hash ^= (unsigned char)data[n];
+        hash *= 1111111111111111111;
+    }
+
+    return hash;
+}
+
+
 #endif /* XS_IMPLEMENTATION */
 #endif /* XS_IMPLEMENTATION */
 
 
 #endif /* _XS_H */
 #endif /* _XS_H */

+ 2 - 1
xs_io.h

@@ -27,7 +27,8 @@ xs_str *xs_readline(FILE *f)
         while ((c = fgetc(f)) != EOF) {
         while ((c = fgetc(f)) != EOF) {
             unsigned char rc = c;
             unsigned char rc = c;
 
 
-            s = xs_append_m(s, (char *)&rc, 1);
+            if (xs_is_string((char *)&rc))
+                s = xs_append_m(s, (char *)&rc, 1);
 
 
             if (c == '\n')
             if (c == '\n')
                 break;
                 break;

+ 6 - 1
xs_match.h

@@ -24,6 +24,7 @@ int xs_match(const char *str, const char *spec)
 retry:
 retry:
 
 
     for (;;) {
     for (;;) {
+        const char *q = spec;
         char c = *str++;
         char c = *str++;
         char p = *spec++;
         char p = *spec++;
 
 
@@ -63,8 +64,12 @@ retry:
                     spec = b_spec;
                     spec = b_spec;
                     str  = ++b_str;
                     str  = ++b_str;
                 }
                 }
-                else
+                else {
+                    if (*q == '|')
+                        spec = q;
+
                     break;
                     break;
+                }
             }
             }
         }
         }
     }
     }

+ 1 - 1
xs_openssl.h

@@ -83,7 +83,7 @@ xs_val *xs_base64_dec(const xs_str *data, int *size)
     s = xs_realloc(s, _xs_blk_size(*size + 1));
     s = xs_realloc(s, _xs_blk_size(*size + 1));
     s[*size] = '\0';
     s[*size] = '\0';
 
 
-    BIO_free_all(mem);
+    BIO_free_all(b64);
 
 
     return s;
     return s;
 }
 }

+ 2 - 0
xs_socket.h

@@ -85,6 +85,8 @@ int xs_socket_server(const char *addr, const char *serv)
       listen(rs, SOMAXCONN);
       listen(rs, SOMAXCONN);
     }
     }
 
 
+    freeaddrinfo(res);
+
 #else /* WITHOUT_GETADDRINFO */
 #else /* WITHOUT_GETADDRINFO */
    struct sockaddr_in host;
    struct sockaddr_in host;
 
 

+ 7 - 1
xs_url.h

@@ -17,12 +17,18 @@ xs_str *xs_url_dec(const char *str)
     xs_str *s = xs_str_new(NULL);
     xs_str *s = xs_str_new(NULL);
 
 
     while (*str) {
     while (*str) {
+        if (!xs_is_string(str))
+            break;
+
         if (*str == '%') {
         if (*str == '%') {
             unsigned int i;
             unsigned int i;
 
 
             if (sscanf(str + 1, "%02x", &i) == 1) {
             if (sscanf(str + 1, "%02x", &i) == 1) {
                 unsigned char uc = i;
                 unsigned char uc = i;
 
 
+                if (!xs_is_string((char *)&uc))
+                    break;
+
                 s = xs_append_m(s, (char *)&uc, 1);
                 s = xs_append_m(s, (char *)&uc, 1);
                 str += 2;
                 str += 2;
             }
             }
@@ -69,7 +75,7 @@ xs_dict *xs_url_vars(const char *str)
 
 
     vars = xs_dict_new();
     vars = xs_dict_new();
 
 
-    if (str != NULL) {
+    if (xs_is_string(str)) {
         /* split by arguments */
         /* split by arguments */
         xs *args = xs_split(str, "&");
         xs *args = xs_split(str, "&");
 
 

+ 1 - 1
xs_version.h

@@ -1 +1 @@
-/* b865e89769aedfdbc61251e94451e9d37579f52e 2025-01-12T16:17:47+01:00 */
+/* 2f43b93e9d2b63360c802e09f4c68adfef74c673 2025-01-28T07:40:50+01:00 */