anntp

a nntp implementation in pure C99
Log | Files | Refs | README | LICENSE

commit beb6534db668aa2d9c620eb9c2914269fc10c4d3
parent 63a867ec2671306eb2c464c241064426087c77e8
Author: Mario Rosell R. Martinez <mario@mariorosell.es>
Date:   Sat, 21 Mar 2026 12:48:36 +0100

Add wrappers over some things

Diffstat:
Manntp.h | 281+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mexamples/nntpsh.c | 7++++++-
2 files changed, 282 insertions(+), 6 deletions(-)

diff --git a/anntp.h b/anntp.h @@ -1,7 +1,7 @@ /** - ** anntp v0.1.0 - public-domain nntp client implementation in C - by Mario Rosell an contributors - ** THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. - ** use at your own risk. + ** anntp v0.1.0dev - public-domain nntp client implementation in C - by Mario Rosell an contributors + ** THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. + ** use at your own risk. ** #include ... ** ** Add: @@ -95,9 +95,9 @@ # if (defined(__GNUC__) && defined(_GNU_SOURCE)) || (defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 200809L) # define ANNTP_STRDUP strdup # else -# define ANNTP_STRDUP anntp__NC__strdup +# define ANNTP_STRDUP ______anntp__NC__strdup______ char* -anntp__NC__strdup(const char* s) { +______anntp__NC__strdup______(const char* s) { size_t len = strlen(s) + 1; char* p = (char*)malloc(len); if (p) memcpy(p, s, len); @@ -116,6 +116,7 @@ anntp__NC__strdup(const char* s) { #define ANNTP_CODE(x) ((AnntpErrCode)(-(x))) typedef unsigned char uchar_t; +typedef long AnntpArticleNumber; enum anntp_state { ANS_OFF, @@ -152,7 +153,26 @@ struct anntp_connection { #endif }; +struct anntp_group { + char* name; + AnntpArticleNumber first; + AnntpArticleNumber last; + long count; + char mode; /* y for posting allowed, n for no posting allowed, m for moderated */ +}; + +struct anntp_article { + AnntpArticleNumber n; + char* id; + char* from; + char* date; + char* body; + char* subject; +}; + typedef struct anntp_connection AnntpConnection; +typedef struct anntp_article AnntpArticle; +typedef struct anntp_group AnntpGroup; typedef int (*AnntpLineCb)(const char* line, void* extra); @@ -164,8 +184,13 @@ ANNTP_API void anntp_freeconn(AnntpConnection* conn); ANNTP_API ssize_t anntp_read(AnntpConnection* conn, uchar_t* buf, size_t bufsize); ANNTP_API ssize_t anntp_write(AnntpConnection* conn, const uchar_t* buf, size_t bufsize); ANNTP_API ssize_t anntp_write_all(AnntpConnection* conn, const uchar_t* buf, size_t bufsize); +ANNTP_API int anntp_group(AnntpConnection* conn, const char* groupname, AnntpGroup* out); +ANNTP_API ssize_t anntp_list(AnntpConnection* conn, AnntpGroup** out_groups); +ANNTP_API ssize_t anntp_article(AnntpConnection* conn, AnntpArticleNumber n, AnntpArticle* out_art); ANNTP_API ssize_t anntp_writeline(AnntpConnection* conn, const char* buf); ANNTP_API ssize_t anntp_readline(AnntpConnection* conn, char* buf, size_t maxlen); +ANNTP_API void anntp_free_article(AnntpArticle* a); +ANNTP_API void anntp_free_groups(AnntpGroup* gs, size_t count); ANNTP_API ssize_t anntp_readdot(AnntpConnection* conn, char* buf, size_t maxlen); ANNTP_API int anntp_readdot_cb(AnntpConnection* conn, AnntpLineCb cb, void* extra); ANNTP_API int anntp_auth(AnntpConnection* conn, const char* login, const char* password); @@ -284,6 +309,225 @@ anntp_freeconn(AnntpConnection* cv) ANNTP_FREE(cv); } +int +anntp_group(AnntpConnection* cv, const char* group, AnntpGroup* out) +{ + if (!cv || !group || !out) + return ANE_PARAMS; + + char cmd[ANNTP_BUFSIZE]; + snprintf(cmd, sizeof(cmd), "GROUP %s", group); + + if (anntp_writeline(cv, cmd) <= 0) + return ANE_IO; + + char line[ANNTP_BUFSIZE]; + if (anntp_readline(cv, line, sizeof(line)) <= 0) + return ANE_IO; + + /* expect a 221 for OK */ + if (strncmp(line, "211", 3) != 0) + return ANE_PROTO; + + int count; + int first, last; + char mode; + char name[128]; + + /* validate */ + if (sscanf(line, "211 %d %d %d %127s", &count, &first, &last, name) != 4) + return ANE_PROTO; /* malformed line */ + + strncpy(name, group, sizeof(name)-1); + + out->name = ANNTP_STRDUP(name); + if (!out->name) + return ANE_IO; + + out->first = (AnntpArticleNumber)first; + out->last = (AnntpArticleNumber)last; + out->count = (long)count; + + out->mode = '?'; /* we can't really know the status of a group without LIST ACTIVE */ + + return ANE_OK; +} + +ssize_t +anntp_list(AnntpConnection* cv, AnntpGroup** out_groups) +{ + if (!cv || !out_groups) return ANNTPE(ANE_PARAMS); + + size_t count = 0; + *out_groups = NULL; + + if (anntp_writeline(cv, "LIST ACTIVE") <= 0) + return ANNTPE(ANE_IO); + + char line[ANNTP_BUFSIZE]; + + if (anntp_readline(cv, line, sizeof(line)) <= 0) + return ANNTPE(ANE_IO); + + size_t cap = 16; + AnntpGroup* groups = (AnntpGroup*)ANNTP_MALLOC(cap * sizeof(AnntpGroup)); + if (!groups) + return ANNTPE(ANE_IO); + + for (;;) { + ssize_t n = anntp_readline(cv, line, sizeof(line)); + if (n <= 0) + goto fail; + + if (strncmp(line, ".\r\n", 3) == 0) + break; + + char name[256] = {0}; + long last = 0, first = 0; + char mode = '?'; + + if (sscanf(line, "%255s %ld %ld %c", name, &last, &first, &mode) != 4) + continue; + + if (count >= cap) { + cap *= 2; + AnntpGroup* tmp = (AnntpGroup*)realloc(groups, cap * sizeof(AnntpGroup)); + if (!tmp) + goto fail; + groups = tmp; + } + + groups[count].name = ANNTP_STRDUP(name); + if (!groups[count].name) + goto fail; + + groups[count].first = (AnntpArticleNumber)first; + groups[count].last = (AnntpArticleNumber)last; + groups[count].mode = mode; + + count++; + } + + *out_groups = groups; + return (ssize_t)count; + +fail: + if (groups) { + for (size_t i = 0; i < count; i++) { + if (groups[i].name) + ANNTP_FREE(groups[i].name); + } + ANNTP_FREE(groups); + } + return ANNTPE(ANE_IO); +} + +struct __ArticleStream { + AnntpArticle* out; + char* body; + size_t len; + size_t cap; + Bool headers_done; +}; + +static int +_article_line_cb(const char* line, void* extra) +{ + struct __ArticleStream* s = (struct __ArticleStream*)extra; + + if (!s->headers_done) { + if (line[0] == '\0') { /* blank line is end of headers */ + s->headers_done = true; + return 0; + } + + if (strncmp(line, "From:", 5) == 0 && !s->out->from) + s->out->from = ANNTP_STRDUP(line + 5); + else if (strncmp(line, "Date:", 5) == 0 && !s->out->date) + s->out->date = ANNTP_STRDUP(line + 5); + else if (strncmp(line, "Subject:", 8) == 0 && !s->out->subject) + s->out->subject = ANNTP_STRDUP(line + 8); + return 0; + } + + size_t line_len = strlen(line); + if (s->len + line_len + 2 >= s->cap) { + s->cap *= 2; + char* tmp = (char*)realloc(s->body, s->cap); + if (!tmp) return 1; /* stop on OOM */ + s->body = tmp; + } + + memcpy(s->body + s->len, line, line_len); + s->len += line_len; + s->body[s->len++] = '\r'; + s->body[s->len++] = '\n'; + + return 0; +} + +ssize_t +anntp_article(AnntpConnection* cv, AnntpArticleNumber n, AnntpArticle* out) +{ + if (!cv || !out) + return ANNTPE(ANE_PARAMS); + + char cmd[128]; + char line[ANNTP_BUFSIZE]; + + snprintf(cmd, sizeof(cmd), "ARTICLE %ld", (long)n); + + if (anntp_writeline(cv, cmd) <= 0) + return ANNTPE(ANE_IO); + + if (anntp_readline(cv, line, sizeof(line)) <= 0) + return ANNTPE(ANE_IO); + + if (strncmp(line, "220", 3) != 0) + return ANNTPE(ANE_PROTO); + + long num = 0; + char msgid[256] = {0}; + sscanf(line, "220 %ld %255s", &num, msgid); + + /* init out */ + out->n = (AnntpArticleNumber)num; + out->id = ANNTP_STRDUP(msgid); + out->from = NULL; + out->date = NULL; + out->body = NULL; + + if (!out->id) + return ANNTPE(ANE_IO); + + /* setup streamer */ + struct __ArticleStream stream; + stream.out = out; + stream.cap = ANNTP_BUFSIZE; + stream.len = 0; + stream.headers_done = false; + stream.body = (char*)ANNTP_MALLOC(stream.cap); + if (!stream.body) + return ANNTPE(ANE_IO); + + ssize_t ret = anntp_readdot_cb(cv, _article_line_cb, &stream); + if (ret < 0) { + ANNTP_FREE(stream.body); + return ret; + } + + /* finalize body */ + if (stream.len > 0) { + stream.body[stream.len] = '\0'; + out->body = stream.body; + } else { + ANNTP_FREE(stream.body); + out->body = NULL; + } + + return stream.len; +} + ssize_t anntp_read(AnntpConnection* cv, uchar_t* buf, size_t len) { @@ -559,5 +803,32 @@ anntp_strerror(AnntpErrCode c) } } +void +anntp_free_article(AnntpArticle* a) +{ + if (!a) return; + if (a->id) { ANNTP_FREE(a->id); a->id = NULL; } + if (a->from) { ANNTP_FREE(a->from); a->from = NULL; } + if (a->date) { ANNTP_FREE(a->date); a->date = NULL; } + if (a->body) { ANNTP_FREE(a->body); a->body = NULL; } + if (a->subject) { ANNTP_FREE(a->subject); a->subject = NULL; } +} + +void +anntp_free_groups(AnntpGroup* groups, size_t count) +{ + if (!groups) return; + + for (size_t i = 0; i < count; i++) { + if (groups[i].name) { + ANNTP_FREE(groups[i].name); + groups[i].name = NULL; + } + } + + ANNTP_FREE(groups); +} + + #endif /* ANNTP_IMPLEMENTATION */ diff --git a/examples/nntpsh.c b/examples/nntpsh.c @@ -16,6 +16,7 @@ #define ANNTP_IMPLEMENTATION #include "../anntp.h" +static char argv0[256]; static char host[0x100]; static char port[0x10]; static bool usetls; @@ -52,13 +53,17 @@ cli(int argc, char** argv) /* copy port */ strncpy(port, argv[2], sizeof(port) - 1); port[sizeof(port) - 1] = '\0'; + + /* copy argv0 */ + strncpy(argv0, argv[0], sizeof(argv0) - 1); + argv0[sizeof(argv0) - 1] = '\0'; } void setup(void) { c = anntp_mkconn(host, port, usetls); if (!c) { - fprintf(stderr, "%s: can't create connection\n"); + fprintf(stderr, "%s: can't create connection\n", argv0); exit(EXIT_FAILURE); }