fmj/fmj.c
2025-10-26 01:12:27 -05:00

328 lines
7.6 KiB
C

#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <errno.h>
#include <dirent.h>
#define LINE_MAX UINT16_MAX
// appended to the input directory to get the path to the feed's config
const char* fmjconfp = ".fmjconf";
const char* fmj_extension = ".fmj";
// .fmjconf and .fmj option names
const char* description = "description";
const char* title = "title";
const char* link = "link";
const char* default_author = "default_author";
const char* author = "author";
const char* category = "category";
const char* comments = "comments";
const char* pub_date = "pub_date";
// global for the default_author from the .fmjconf
char* default_author_name = NULL;
void print_usage(void) {
puts("usage:\nfmj dir [output file]");
}
// caller must free returned ptr
char* join_path(const char* a, const char* b) {
char* buf = malloc(strlen(a) + strlen(b) + 2);
strcpy(buf, a);
strncat(buf, "/", 2);
strncat(buf, b, strlen(b) + 1);
return buf;
}
// parses an option from `line` that is formatted like this:
// "title = contents"
//
// it will ignore the whitespace around the "=",
// and return a pointer to the beginning of "contents",
// or return NULL if it didn't match.
const char* parse_option(const char* line, const char* option) {
size_t optlen = strlen(option);
// if it doesn't have an "=" and then at least one
// char of content then its not valid
if (strlen(line) < optlen + 2) {
return NULL;
}
if (strncmp(line, option, optlen) != 0) {
return NULL;
}
const char* cur = line + optlen;
bool foundequals = false;
while (true) {
if (isblank(*cur)) {
cur++;
continue;
}
if (!foundequals && *cur == '=') {
foundequals = true;
cur++;
continue;
}
// empty option is invalid
if (foundequals && *cur != '\n') {
break;
}
// at this point any valid cases have been exhausted
return NULL;
}
return cur;
}
void write_without_newline(const char* content, FILE* output) {
while (*content != '\0' && *content != '\n') {
fputc(*content, output);
content++;
}
}
/* broken old version with out of bounds read because of `||` xd
void write_without_newline(FILE* output, const char* content) {
while (*content != '\0' || *content != '\n') {
fputc(*content, output);
content++;
}
}*/
void write_feed_header(FILE* output, FILE* fmjconf) {
fputs("<?xml version=\"1.0\" encoding=\"UTF-8\" ?><rss version=\"2.0\"><channel>", output);
bool found_title = false;
bool found_link = false;
bool found_description = false;
bool found_default_author = false;
char buf[LINE_MAX];
if (fmjconf != NULL) {
while (fgets(buf, LINE_MAX, fmjconf) != NULL) {
const char* opt;
if ((opt = parse_option(buf, title)) != NULL && !found_title) {
fputs("<title>", output);
write_without_newline(opt, output);
fputs("</title>", output);
found_title = true;
continue;
}
if ((opt = parse_option(buf, link)) != NULL && !found_link) {
fputs("<link>", output);
write_without_newline(opt, output);
fputs("</link>", output);
found_link = true;
continue;
}
if ((opt = parse_option(buf, description)) != NULL && !found_description) {
fputs("<description>", output);
write_without_newline(opt, output);
fputs("</description>", output);
found_description = true;
continue;
}
if ((opt = parse_option(buf, default_author)) != NULL && !found_default_author) {
default_author_name = malloc(strlen(opt) + 1);
strcpy(default_author_name, opt);
found_default_author = true;
continue;
}
}
}
if (!found_title) {
fputs("<title>fmj generated feed</title>", output);
}
if (!found_link) {
fputs("<link>http://example.com/</link>", output);
}
if (!found_description) {
fputs("<description>an rss feed generated by fmj</description>", output);
}
fputs("<generator>fmj</generator>", output);
}
void write_feed_item(FILE* output, const char* dir, const char* entryname) {
if (dir == NULL || entryname == NULL) {
return;
}
// only consider .fmj files
size_t namelen = strlen(entryname);
size_t extlen = strlen(fmj_extension);
if (namelen < extlen) {
return;
}
if (strncmp(entryname + namelen - extlen, fmj_extension, extlen) != 0) {
return;
}
char* path = join_path(dir, entryname);
FILE* file = fopen(path, "r");
if (file == NULL) {
free(path);
return;
}
char buf[LINE_MAX];
// make sure it has either a title or description
bool has_requirements = false;
while (fgets(buf, LINE_MAX, file) != NULL) {
if (parse_option(buf, title) || parse_option(buf, description)) {
has_requirements = true;
break;
}
}
if (!has_requirements) {
return;
}
rewind(file);
bool found_title = false;
bool found_description = false;
bool found_link = false;
bool found_pub_date = false;
bool found_author = false;
fputs("<item>", output);
while (fgets(buf, LINE_MAX, file) != NULL) {
const char* opt;
if ((opt = parse_option(buf, title)) != NULL && !found_title) {
fputs("<title>", output);
write_without_newline(opt, output);
fputs("</title>", output);
}
if ((opt = parse_option(buf, link)) != NULL && !found_link) {
fputs("<link>", output);
write_without_newline(opt, output);
fputs("</link>", output);
}
if ((opt = parse_option(buf, description)) != NULL && !found_description) {
fputs("<description>", output);
write_without_newline(opt, output);
fputs("</description>", output);
}
if ((opt = parse_option(buf, author)) != NULL && !found_author) {
fputs("<author>", output);
write_without_newline(opt, output);
fputs("</author>", output);
}
if ((opt = parse_option(buf, pub_date)) != NULL && !found_pub_date) {
fputs("<pubDate>", output);
write_without_newline(opt, output);
fputs("</pubDate>", output);
}
if ((opt = parse_option(buf, category)) != NULL) {
fputs("<category>", output);
write_without_newline(opt, output);
fputs("</category>", output);
}
}
if (!found_author && default_author_name != NULL) {
fputs("<author>", output);
write_without_newline(default_author_name, output);
fputs("</author>", output);
}
fputs("</item>", output);
free(path);
}
void write_feed_footer(FILE* output) {
fputs("</channel></rss>", output);
}
int main(int argc, char** argv) {
if (argc < 2) {
print_usage();
return 1;
}
DIR* dir = opendir(argv[1]);
if (dir == NULL) {
puts("failed to open the input directory!!!");
return 1;
}
char* outfilepath = "rss.xml";
if (argc >= 3) {
outfilepath = argv[2];
}
FILE* output;
if (*outfilepath == '-') {
output = stdout;
} else {
output = fopen(outfilepath, "w");
if (output == NULL) {
puts("failed to open output file!!!!");
return 1;
}
}
char* fmjconfpath = join_path(argv[1], fmjconfp);
FILE* fmjconf = fopen(fmjconfpath, "r");
write_feed_header(output, fmjconf);
if (fmjconf != NULL) {
fclose(fmjconf);
}
free(fmjconfpath);
struct dirent* entry;
while (true) {
errno = 0;
entry = readdir(dir);
if (entry != NULL) {
write_feed_item(output, argv[1], entry->d_name);
} else {
if (errno == 0) {
closedir(dir);
break;
}
// i am going to assume getting here is bad and exit
puts("assertion failed, readdir errored.");
return 1;
}
}
write_feed_footer(output);
if (output != stdout) {
fclose(output);
}
}