328 lines
7.6 KiB
C
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);
|
|
}
|
|
}
|