]> infiniteadaptability.org Git - seeder/commitdiff
initial design complete
authoralex <[email protected]>
Wed, 7 Apr 2021 03:38:34 +0000 (20:38 -0700)
committeralex <[email protected]>
Tue, 22 Jun 2021 00:08:56 +0000 (17:08 -0700)
created seederd
created init scripts which interface with systemd
created rss helper programs to generate feeds

78 files changed:
.gitignore [new file with mode: 0644]
Dockerfile [deleted file]
Makefile [deleted file]
Makefile.am [new file with mode: 0644]
README.txt [new file with mode: 0644]
configure.ac [new file with mode: 0644]
entrypoint.sh [new file with mode: 0755]
init/Makefile.am [new file with mode: 0644]
init/remove-nginx.sh [new file with mode: 0755]
init/remove-systemd.sh [new file with mode: 0755]
init/remove.sh [new file with mode: 0755]
init/setup-nginx.sh [new file with mode: 0755]
init/setup-systemd.sh [new file with mode: 0755]
init/setup.sh [new file with mode: 0755]
m4/m4_ax_cxx_compile_stdcxx.m4 [new file with mode: 0644]
rss/Makefile.am [new file with mode: 0644]
rss/inc/add.h [new file with mode: 0644]
rss/inc/default.h [new file with mode: 0644]
rss/inc/escape.h [new file with mode: 0644]
rss/inc/info.h [new file with mode: 0644]
rss/inc/log.h [new file with mode: 0644]
rss/inc/main.h [new file with mode: 0644]
rss/inc/meta.h [new file with mode: 0644]
rss/inc/next.h [new file with mode: 0644]
rss/inc/opt.h [new file with mode: 0644]
rss/inc/rss.h [new file with mode: 0644]
rss/inc/util.h [new file with mode: 0644]
rss/src/add.c [new file with mode: 0644]
rss/src/default.c [new file with mode: 0644]
rss/src/escape.c [new file with mode: 0644]
rss/src/info.c [new file with mode: 0644]
rss/src/log.c [new file with mode: 0644]
rss/src/main.c [new file with mode: 0644]
rss/src/meta.c [new file with mode: 0644]
rss/src/next.c [new file with mode: 0644]
rss/src/opt/desc.c [new file with mode: 0644]
rss/src/opt/feed.c [new file with mode: 0644]
rss/src/opt/link.c [new file with mode: 0644]
rss/src/opt/meta.c [new file with mode: 0644]
rss/src/opt/name.c [new file with mode: 0644]
rss/src/opt/out.c [new file with mode: 0644]
rss/src/util/now.c [new file with mode: 0644]
rss/test/add.tests.c [new file with mode: 0644]
rss/test/add.tests.h [new file with mode: 0644]
rss/test/escape.tests.c [new file with mode: 0644]
rss/test/escape.tests.h [new file with mode: 0644]
rss/test/info.tests.c [new file with mode: 0644]
rss/test/info.tests.h [new file with mode: 0644]
rss/test/meta.tests.c [new file with mode: 0644]
rss/test/meta.tests.h [new file with mode: 0644]
rss/test/next.tests.c [new file with mode: 0644]
rss/test/next.tests.h [new file with mode: 0644]
rss/test/opt.desc.tests.c [new file with mode: 0644]
rss/test/opt.desc.tests.h [new file with mode: 0644]
rss/test/opt.feed.tests.c [new file with mode: 0644]
rss/test/opt.feed.tests.h [new file with mode: 0644]
rss/test/opt.link.tests.c [new file with mode: 0644]
rss/test/opt.link.tests.h [new file with mode: 0644]
rss/test/opt.meta.tests.c [new file with mode: 0644]
rss/test/opt.meta.tests.h [new file with mode: 0644]
rss/test/opt.name.tests.c [new file with mode: 0644]
rss/test/opt.name.tests.h [new file with mode: 0644]
rss/test/opt.out.tests.c [new file with mode: 0644]
rss/test/opt.out.tests.h [new file with mode: 0644]
rss/test/test_utils.c [new file with mode: 0644]
rss/test/test_utils.h [new file with mode: 0644]
rss/test/util.now.tests.c [new file with mode: 0644]
rss/test/util.now.tests.h [new file with mode: 0644]
seederd/Makefile.am [new file with mode: 0644]
seederd/inc/consts.h [new file with mode: 0644]
seederd/inc/feed.hpp [new file with mode: 0644]
seederd/inc/main.h [new file with mode: 0644]
seederd/inc/seed.hpp [new file with mode: 0644]
seederd/inc/torrent.hpp [new file with mode: 0644]
seederd/src/feed.cpp [new file with mode: 0644]
seederd/src/main.cpp [new file with mode: 0644]
seederd/src/seed.cpp [new file with mode: 0644]
seederd/src/torrent.cpp [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..92947ef
--- /dev/null
@@ -0,0 +1,30 @@
+# autoconf/make
+autom4te.cache/
+autoscan.log
+aclocal.m4
+build-aux/
+config.h.in
+config.h
+config.h.in~
+config.log
+config.status
+configure
+configure.scan
+*.in
+*.in~
+**/.deps
+**/.dirstamp
+**/Makefile
+**/Makefile.in
+stamp-h1
+
+# build objects
+*.o
+*.log
+*.trs
+*.tests
+
+# binaries
+rss/rss-create
+rss/*_tests
+seederd/seederd
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644 (file)
index 31d55c5..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM debian:latest
-
-ENTRYPOINT bash
diff --git a/Makefile b/Makefile
deleted file mode 100644 (file)
index d0f616a..0000000
--- a/Makefile
+++ /dev/null
@@ -1,22 +0,0 @@
-IMAGE_NAME=seeder
-CONTAINER_NAME=seeder-test
-build:
-       docker build -t $(IMAGE_NAME) .
-
-run: build
-       docker run -d \
-               -p "8080:8080" \
-               --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \
-               --name $(CONTAINER_NAME) \
-               $(IMAGE_NAME)
-
-start: build
-       -docker run -it --rm \
-               --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \
-               -p "8080:8080" \
-               $(IMAGE_NAME)
-
-stop:
-       docker stop $(CONTAINER_NAME)
-       docker rm $(CONTAINER_NAME)
-
diff --git a/Makefile.am b/Makefile.am
new file mode 100644 (file)
index 0000000..eac46e1
--- /dev/null
@@ -0,0 +1 @@
+SUBDIRS = rss seederd init
diff --git a/README.txt b/README.txt
new file mode 100644 (file)
index 0000000..eb30f61
--- /dev/null
@@ -0,0 +1,24 @@
+Compile-Dependencies:
+libtorrent-rasterbar-dev
+inotify-tools
+
+Optional Dependencies:
+systemd
+nginx-light
+
+Build Dependencies:
+autoconf
+automake
+gcc
+g++
+
+Testing Dependencies:
+valgrind
+
+To Build+Install:
+autoreconf -vif
+./configure
+make
+make install
+systemctl enable seederd
+systemctl start seederd
diff --git a/configure.ac b/configure.ac
new file mode 100644 (file)
index 0000000..29dfe87
--- /dev/null
@@ -0,0 +1,83 @@
+#                                               -*- Autoconf -*-
+# Process this file with autoconf to produce a configure script.
+
+AC_PREREQ([2.69])
+AC_INIT([seeder], [0.0.0])
+
+# Store build files not in main directory
+AC_CONFIG_AUX_DIR([build-aux])
+AC_CONFIG_MACRO_DIR([m4])
+
+AM_INIT_AUTOMAKE([foreign subdir-objects nostdinc -Wall -Werror])
+
+AC_ARG_ENABLE([tests],
+  [AS_HELP_STRING([--disable-tests],
+  [disable tests (enabled by default)])],
+  [enable_tests=$enableval],
+  [enable_tests=yes])
+
+AC_ARG_ENABLE([memcheck],
+  [AS_HELP_STRING([--disable-memcheck],
+  [disable valgrind (enabled by default)])],
+  [enable_memcheck=$enableval],
+  [enable_memcheck=yes])
+
+AC_PATH_PROG([VALGRIND], [valgrind])
+AM_CONDITIONAL([HAVE_VALGRIND], [test -n "$VALGRIND"])
+AC_PATH_PROG([NGINX], [nginx])
+AM_CONDITIONAL([HAVE_NGINX], [test -n "$NGINX"])
+AC_PATH_PROG([SYSTEMD], [systemd])
+AM_CONDITIONAL([HAVE_SYSTEMD], [test -n "$SYSTEMD"])
+
+AC_PATH_PROG([INOTIFYWAIT], [inotifywait])
+if test -z "$INOTIFYWAIT"; then
+       AC_MSG_ERROR("inotifywait required");
+fi
+
+dnl disable memcheck if valgrind not found
+if test "x$enable_memcheck" != "xno"; then
+  if test -z "$VALGRIND"; then
+    enable_memcheck=no
+  fi
+fi
+
+AC_MSG_CHECKING([if memcheck should be enabled])
+if test x$enable_memcheck != xno; then
+  AC_MSG_RESULT(yes)
+else
+  AC_MSG_RESULT(no)
+fi
+
+AC_ARG_VAR(SEEDER_DOMAIN,[hostname from which seeder is serving])
+
+AM_CONDITIONAL([ENABLE_MEMCHECK],[test x$enable_memcheck = xyes])
+
+
+# Checks for programs.
+AC_PROG_CC
+AC_PROG_CXX
+AC_PROG_LN_S
+
+# Checks for libraries.
+AC_CHECK_LIB([torrent-rasterbar], [main],,AC_MSG_ERROR(libtorrent-rasterbar required))
+AC_CHECK_LIB([boost_system], [main],,AC_MSG_ERROR(boost_system required))
+AC_CHECK_LIB([pthread], [main],,AC_MSG_ERROR(pthread required))
+
+# Checks for header files.
+AC_CHECK_HEADERS([stddef.h stdlib.h string.h])
+
+# Checks for typedefs, structures, and compiler characteristics.
+AC_CHECK_HEADER_STDBOOL
+AC_TYPE_SIZE_T
+
+# Checks for library functions.
+AC_FUNC_MALLOC
+AC_CHECK_FUNCS([atexit mkdir strchr strpbrk strrchr])
+
+AX_CXX_COMPILE_STDCXX([17], [noext], [mandatory])
+
+AC_CONFIG_FILES([Makefile
+                init/Makefile
+                 rss/Makefile
+                seederd/Makefile])
+AC_OUTPUT
diff --git a/entrypoint.sh b/entrypoint.sh
new file mode 100755 (executable)
index 0000000..3d53fcb
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+rm /etc/ssh/ssh_host_*
+dpkg-reconfigure openssh-server
+
+service ssh start
+service nginx start
+service cron start
+
+monitor-seederd &
+
+seederd
diff --git a/init/Makefile.am b/init/Makefile.am
new file mode 100644 (file)
index 0000000..f21f601
--- /dev/null
@@ -0,0 +1,17 @@
+install-exec-local:
+       ./setup.sh $(bindir) $(localstatedir)/seeder $(SEEDER_DOMAIN)
+if HAVE_NGINX
+       ./setup-nginx.sh $(localstatedir)/seeder
+endif
+if HAVE_SYSTEMD
+       -./setup-systemd.sh $(localstatedir)/seeder
+endif
+
+uninstall-local:
+       -./remove.sh $(bindir) $(localstatedir)/seeder
+if HAVE_NGINX
+       -./remove-nginx.sh $(localstatedir)/seeder
+endif
+if HAVE_SYSTEMD
+       -./remove-systemd.sh
+endif
diff --git a/init/remove-nginx.sh b/init/remove-nginx.sh
new file mode 100755 (executable)
index 0000000..3bc3470
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+rm -f "$1/index.html"
+
+rm -f /etc/nginx/sites-enabled/feeds
+rm -f /etc/nginx/sites-available/feeds
+
+systemctl reload nginx
diff --git a/init/remove-systemd.sh b/init/remove-systemd.sh
new file mode 100755 (executable)
index 0000000..1ab32d8
--- /dev/null
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+systemctl stop seederd
+systemctl disable seederd
+rm -f /lib/systemd/system/seederd.service
+
+systemctl stop seederd-monitor
+systemctl disable seederd-monitor
+rm -f /lib/systemd/system/seederd-monitor.service
+
+systemctl daemon-reload
+systemctl reset-failed
diff --git a/init/remove.sh b/init/remove.sh
new file mode 100755 (executable)
index 0000000..48f00e7
--- /dev/null
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+rm -f "$1/update-feeds"
+
+rm -rf "$2"
diff --git a/init/setup-nginx.sh b/init/setup-nginx.sh
new file mode 100755 (executable)
index 0000000..c07d180
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+mkdir -p "$1"
+
+cat > "$1/index.html" << EOF
+<html>
+<p>This is the homepage of seeder.</p>
+<p><a href="feeds/">Feeds</a></p>
+<p><a href="torrents/">Torrent Files</a></p>
+</html>
+EOF
+
+# setup nginx
+rm -f /etc/nginx/sites-enabled/default
+cat > /etc/nginx/sites-available/feeds << EOF
+server {
+       listen 80 default_server;
+       listen [::]:80;
+
+       root $1;
+       index index.html;
+
+       location /feeds {
+               autoindex on;
+       }
+
+       location /torrents {
+               autoindex on;
+       }
+}
+EOF
+ln -sf /etc/nginx/sites-available/feeds /etc/nginx/sites-enabled/feeds
+
+set +e; systemctl reload nginx; set -e;
diff --git a/init/setup-systemd.sh b/init/setup-systemd.sh
new file mode 100755 (executable)
index 0000000..917a7ea
--- /dev/null
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+cat > /lib/systemd/system/seederd.service << EOF
+[Unit]
+Description=Seeder daemon
+After=network.target
+
+[Service]
+ExecStartPre=mkdir -p $1
+ExecStart=seederd
+ExecReload=kill -HUP \$MAINPID
+
+Type=simple
+Restart=always
+TimeoutStopSec=60
+
+# Hardening measures
+PrivateTmp=true
+ProtectSystem=full
+ProtectHome=true
+NoNewPrivileges=true
+PrivateDevices=true
+ReadWritePaths=$1
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+cat > /lib/systemd/system/seederd-monitor.service << EOF
+[Unit]
+Description=Seeder Monitor Service
+After=seederd.service
+
+[Service]
+ExecStartPre=mkdir -p "$1/data"
+ExecStart=inotifywait -e modify,create,delete,move -r "$1/data"
+ExecStopPost=systemctl reload seederd
+Type=simple
+Restart=on-success
+RestartSec=500ms
+
+[Install]
+RequiredBy=seederd.service
+EOF
diff --git a/init/setup.sh b/init/setup.sh
new file mode 100755 (executable)
index 0000000..38f3e6b
--- /dev/null
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+mkdir -p "$1"
+mkdir -p "$2"
+
+cat > "$1/update-feeds" << EOF
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+mkdir -p $2/feeds
+
+FEEDS=(\$(find $2/data -maxdepth 1 -mindepth 1 -type d))
+
+for feed in "\${FEEDS[@]}"; do
+       NAME=\$(basename \$feed)
+       echo "building feed: \$NAME"
+       (cd \$feed; find . -mindepth 1 -maxdepth 1 -not -name ".*" -not -path "**/.*" | sed "s|./||" | sort | rss-create -o $2/feeds/\$NAME.xml -l "$3" -f "$3/torrents/" -n \$NAME -m "../.\$NAME.meta")
+done
+EOF
+
+chmod +x "$1/update-feeds"
diff --git a/m4/m4_ax_cxx_compile_stdcxx.m4 b/m4/m4_ax_cxx_compile_stdcxx.m4
new file mode 100644 (file)
index 0000000..9413da6
--- /dev/null
@@ -0,0 +1,962 @@
+# ===========================================================================
+#  https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_CXX_COMPILE_STDCXX(VERSION, [ext|noext], [mandatory|optional])
+#
+# DESCRIPTION
+#
+#   Check for baseline language coverage in the compiler for the specified
+#   version of the C++ standard.  If necessary, add switches to CXX and
+#   CXXCPP to enable support.  VERSION may be '11' (for the C++11 standard)
+#   or '14' (for the C++14 standard).
+#
+#   The second argument, if specified, indicates whether you insist on an
+#   extended mode (e.g. -std=gnu++11) or a strict conformance mode (e.g.
+#   -std=c++11).  If neither is specified, you get whatever works, with
+#   preference for no added switch, and then for an extended mode.
+#
+#   The third argument, if specified 'mandatory' or if left unspecified,
+#   indicates that baseline support for the specified C++ standard is
+#   required and that the macro should error out if no mode with that
+#   support is found.  If specified 'optional', then configuration proceeds
+#   regardless, after defining HAVE_CXX${VERSION} if and only if a
+#   supporting mode is found.
+#
+# LICENSE
+#
+#   Copyright (c) 2008 Benjamin Kosnik <[email protected]>
+#   Copyright (c) 2012 Zack Weinberg <[email protected]>
+#   Copyright (c) 2013 Roy Stogner <[email protected]>
+#   Copyright (c) 2014, 2015 Google Inc.; contributed by Alexey Sokolov <[email protected]>
+#   Copyright (c) 2015 Paul Norman <[email protected]>
+#   Copyright (c) 2015 Moritz Klammler <[email protected]>
+#   Copyright (c) 2016, 2018 Krzesimir Nowak <[email protected]>
+#   Copyright (c) 2019 Enji Cooper <[email protected]>
+#   Copyright (c) 2020 Jason Merrill <[email protected]>
+#
+#   Copying and distribution of this file, with or without modification, are
+#   permitted in any medium without royalty provided the copyright notice
+#   and this notice are preserved.  This file is offered as-is, without any
+#   warranty.
+
+#serial 12
+
+dnl  This macro is based on the code from the AX_CXX_COMPILE_STDCXX_11 macro
+dnl  (serial version number 13).
+
+AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl
+  m4_if([$1], [11], [ax_cxx_compile_alternatives="11 0x"],
+        [$1], [14], [ax_cxx_compile_alternatives="14 1y"],
+        [$1], [17], [ax_cxx_compile_alternatives="17 1z"],
+        [m4_fatal([invalid first argument `$1' to AX_CXX_COMPILE_STDCXX])])dnl
+  m4_if([$2], [], [],
+        [$2], [ext], [],
+        [$2], [noext], [],
+        [m4_fatal([invalid second argument `$2' to AX_CXX_COMPILE_STDCXX])])dnl
+  m4_if([$3], [], [ax_cxx_compile_cxx$1_required=true],
+        [$3], [mandatory], [ax_cxx_compile_cxx$1_required=true],
+        [$3], [optional], [ax_cxx_compile_cxx$1_required=false],
+        [m4_fatal([invalid third argument `$3' to AX_CXX_COMPILE_STDCXX])])
+  AC_LANG_PUSH([C++])dnl
+  ac_success=no
+
+  m4_if([$2], [], [dnl
+    AC_CACHE_CHECK(whether $CXX supports C++$1 features by default,
+                  ax_cv_cxx_compile_cxx$1,
+      [AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+        [ax_cv_cxx_compile_cxx$1=yes],
+        [ax_cv_cxx_compile_cxx$1=no])])
+    if test x$ax_cv_cxx_compile_cxx$1 = xyes; then
+      ac_success=yes
+    fi])
+
+  m4_if([$2], [noext], [], [dnl
+  if test x$ac_success = xno; then
+    for alternative in ${ax_cxx_compile_alternatives}; do
+      switch="-std=gnu++${alternative}"
+      cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch])
+      AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch,
+                     $cachevar,
+        [ac_save_CXX="$CXX"
+         CXX="$CXX $switch"
+         AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+          [eval $cachevar=yes],
+          [eval $cachevar=no])
+         CXX="$ac_save_CXX"])
+      if eval test x\$$cachevar = xyes; then
+        CXX="$CXX $switch"
+        if test -n "$CXXCPP" ; then
+          CXXCPP="$CXXCPP $switch"
+        fi
+        ac_success=yes
+        break
+      fi
+    done
+  fi])
+
+  m4_if([$2], [ext], [], [dnl
+  if test x$ac_success = xno; then
+    dnl HP's aCC needs +std=c++11 according to:
+    dnl http://h21007.www2.hp.com/portal/download/files/unprot/aCxx/PDF_Release_Notes/769149-001.pdf
+    dnl Cray's crayCC needs "-h std=c++11"
+    for alternative in ${ax_cxx_compile_alternatives}; do
+      for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}"; do
+        cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch])
+        AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch,
+                       $cachevar,
+          [ac_save_CXX="$CXX"
+           CXX="$CXX $switch"
+           AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+            [eval $cachevar=yes],
+            [eval $cachevar=no])
+           CXX="$ac_save_CXX"])
+        if eval test x\$$cachevar = xyes; then
+          CXX="$CXX $switch"
+          if test -n "$CXXCPP" ; then
+            CXXCPP="$CXXCPP $switch"
+          fi
+          ac_success=yes
+          break
+        fi
+      done
+      if test x$ac_success = xyes; then
+        break
+      fi
+    done
+  fi])
+  AC_LANG_POP([C++])
+  if test x$ax_cxx_compile_cxx$1_required = xtrue; then
+    if test x$ac_success = xno; then
+      AC_MSG_ERROR([*** A compiler with support for C++$1 language features is required.])
+    fi
+  fi
+  if test x$ac_success = xno; then
+    HAVE_CXX$1=0
+    AC_MSG_NOTICE([No compiler with C++$1 support was found])
+  else
+    HAVE_CXX$1=1
+    AC_DEFINE(HAVE_CXX$1,1,
+              [define if the compiler supports basic C++$1 syntax])
+  fi
+  AC_SUBST(HAVE_CXX$1)
+])
+
+
+dnl  Test body for checking C++11 support
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_11],
+  _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+)
+
+
+dnl  Test body for checking C++14 support
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_14],
+  _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+  _AX_CXX_COMPILE_STDCXX_testbody_new_in_14
+)
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_17],
+  _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+  _AX_CXX_COMPILE_STDCXX_testbody_new_in_14
+  _AX_CXX_COMPILE_STDCXX_testbody_new_in_17
+)
+
+dnl  Tests for new features in C++11
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_11], [[
+
+// If the compiler admits that it is not ready for C++11, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201103L
+
+#error "This is not a C++11 compiler"
+
+#else
+
+namespace cxx11
+{
+
+  namespace test_static_assert
+  {
+
+    template <typename T>
+    struct check
+    {
+      static_assert(sizeof(int) <= sizeof(T), "not big enough");
+    };
+
+  }
+
+  namespace test_final_override
+  {
+
+    struct Base
+    {
+      virtual ~Base() {}
+      virtual void f() {}
+    };
+
+    struct Derived : public Base
+    {
+      virtual ~Derived() override {}
+      virtual void f() override {}
+    };
+
+  }
+
+  namespace test_double_right_angle_brackets
+  {
+
+    template < typename T >
+    struct check {};
+
+    typedef check<void> single_type;
+    typedef check<check<void>> double_type;
+    typedef check<check<check<void>>> triple_type;
+    typedef check<check<check<check<void>>>> quadruple_type;
+
+  }
+
+  namespace test_decltype
+  {
+
+    int
+    f()
+    {
+      int a = 1;
+      decltype(a) b = 2;
+      return a + b;
+    }
+
+  }
+
+  namespace test_type_deduction
+  {
+
+    template < typename T1, typename T2 >
+    struct is_same
+    {
+      static const bool value = false;
+    };
+
+    template < typename T >
+    struct is_same<T, T>
+    {
+      static const bool value = true;
+    };
+
+    template < typename T1, typename T2 >
+    auto
+    add(T1 a1, T2 a2) -> decltype(a1 + a2)
+    {
+      return a1 + a2;
+    }
+
+    int
+    test(const int c, volatile int v)
+    {
+      static_assert(is_same<int, decltype(0)>::value == true, "");
+      static_assert(is_same<int, decltype(c)>::value == false, "");
+      static_assert(is_same<int, decltype(v)>::value == false, "");
+      auto ac = c;
+      auto av = v;
+      auto sumi = ac + av + 'x';
+      auto sumf = ac + av + 1.0;
+      static_assert(is_same<int, decltype(ac)>::value == true, "");
+      static_assert(is_same<int, decltype(av)>::value == true, "");
+      static_assert(is_same<int, decltype(sumi)>::value == true, "");
+      static_assert(is_same<int, decltype(sumf)>::value == false, "");
+      static_assert(is_same<int, decltype(add(c, v))>::value == true, "");
+      return (sumf > 0.0) ? sumi : add(c, v);
+    }
+
+  }
+
+  namespace test_noexcept
+  {
+
+    int f() { return 0; }
+    int g() noexcept { return 0; }
+
+    static_assert(noexcept(f()) == false, "");
+    static_assert(noexcept(g()) == true, "");
+
+  }
+
+  namespace test_constexpr
+  {
+
+    template < typename CharT >
+    unsigned long constexpr
+    strlen_c_r(const CharT *const s, const unsigned long acc) noexcept
+    {
+      return *s ? strlen_c_r(s + 1, acc + 1) : acc;
+    }
+
+    template < typename CharT >
+    unsigned long constexpr
+    strlen_c(const CharT *const s) noexcept
+    {
+      return strlen_c_r(s, 0UL);
+    }
+
+    static_assert(strlen_c("") == 0UL, "");
+    static_assert(strlen_c("1") == 1UL, "");
+    static_assert(strlen_c("example") == 7UL, "");
+    static_assert(strlen_c("another\0example") == 7UL, "");
+
+  }
+
+  namespace test_rvalue_references
+  {
+
+    template < int N >
+    struct answer
+    {
+      static constexpr int value = N;
+    };
+
+    answer<1> f(int&)       { return answer<1>(); }
+    answer<2> f(const int&) { return answer<2>(); }
+    answer<3> f(int&&)      { return answer<3>(); }
+
+    void
+    test()
+    {
+      int i = 0;
+      const int c = 0;
+      static_assert(decltype(f(i))::value == 1, "");
+      static_assert(decltype(f(c))::value == 2, "");
+      static_assert(decltype(f(0))::value == 3, "");
+    }
+
+  }
+
+  namespace test_uniform_initialization
+  {
+
+    struct test
+    {
+      static const int zero {};
+      static const int one {1};
+    };
+
+    static_assert(test::zero == 0, "");
+    static_assert(test::one == 1, "");
+
+  }
+
+  namespace test_lambdas
+  {
+
+    void
+    test1()
+    {
+      auto lambda1 = [](){};
+      auto lambda2 = lambda1;
+      lambda1();
+      lambda2();
+    }
+
+    int
+    test2()
+    {
+      auto a = [](int i, int j){ return i + j; }(1, 2);
+      auto b = []() -> int { return '0'; }();
+      auto c = [=](){ return a + b; }();
+      auto d = [&](){ return c; }();
+      auto e = [a, &b](int x) mutable {
+        const auto identity = [](int y){ return y; };
+        for (auto i = 0; i < a; ++i)
+          a += b--;
+        return x + identity(a + b);
+      }(0);
+      return a + b + c + d + e;
+    }
+
+    int
+    test3()
+    {
+      const auto nullary = [](){ return 0; };
+      const auto unary = [](int x){ return x; };
+      using nullary_t = decltype(nullary);
+      using unary_t = decltype(unary);
+      const auto higher1st = [](nullary_t f){ return f(); };
+      const auto higher2nd = [unary](nullary_t f1){
+        return [unary, f1](unary_t f2){ return f2(unary(f1())); };
+      };
+      return higher1st(nullary) + higher2nd(nullary)(unary);
+    }
+
+  }
+
+  namespace test_variadic_templates
+  {
+
+    template <int...>
+    struct sum;
+
+    template <int N0, int... N1toN>
+    struct sum<N0, N1toN...>
+    {
+      static constexpr auto value = N0 + sum<N1toN...>::value;
+    };
+
+    template <>
+    struct sum<>
+    {
+      static constexpr auto value = 0;
+    };
+
+    static_assert(sum<>::value == 0, "");
+    static_assert(sum<1>::value == 1, "");
+    static_assert(sum<23>::value == 23, "");
+    static_assert(sum<1, 2>::value == 3, "");
+    static_assert(sum<5, 5, 11>::value == 21, "");
+    static_assert(sum<2, 3, 5, 7, 11, 13>::value == 41, "");
+
+  }
+
+  // http://stackoverflow.com/questions/13728184/template-aliases-and-sfinae
+  // Clang 3.1 fails with headers of libstd++ 4.8.3 when using std::function
+  // because of this.
+  namespace test_template_alias_sfinae
+  {
+
+    struct foo {};
+
+    template<typename T>
+    using member = typename T::member_type;
+
+    template<typename T>
+    void func(...) {}
+
+    template<typename T>
+    void func(member<T>*) {}
+
+    void test();
+
+    void test() { func<foo>(0); }
+
+  }
+
+}  // namespace cxx11
+
+#endif  // __cplusplus >= 201103L
+
+]])
+
+
+dnl  Tests for new features in C++14
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_14], [[
+
+// If the compiler admits that it is not ready for C++14, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201402L
+
+#error "This is not a C++14 compiler"
+
+#else
+
+namespace cxx14
+{
+
+  namespace test_polymorphic_lambdas
+  {
+
+    int
+    test()
+    {
+      const auto lambda = [](auto&&... args){
+        const auto istiny = [](auto x){
+          return (sizeof(x) == 1UL) ? 1 : 0;
+        };
+        const int aretiny[] = { istiny(args)... };
+        return aretiny[0];
+      };
+      return lambda(1, 1L, 1.0f, '1');
+    }
+
+  }
+
+  namespace test_binary_literals
+  {
+
+    constexpr auto ivii = 0b0000000000101010;
+    static_assert(ivii == 42, "wrong value");
+
+  }
+
+  namespace test_generalized_constexpr
+  {
+
+    template < typename CharT >
+    constexpr unsigned long
+    strlen_c(const CharT *const s) noexcept
+    {
+      auto length = 0UL;
+      for (auto p = s; *p; ++p)
+        ++length;
+      return length;
+    }
+
+    static_assert(strlen_c("") == 0UL, "");
+    static_assert(strlen_c("x") == 1UL, "");
+    static_assert(strlen_c("test") == 4UL, "");
+    static_assert(strlen_c("another\0test") == 7UL, "");
+
+  }
+
+  namespace test_lambda_init_capture
+  {
+
+    int
+    test()
+    {
+      auto x = 0;
+      const auto lambda1 = [a = x](int b){ return a + b; };
+      const auto lambda2 = [a = lambda1(x)](){ return a; };
+      return lambda2();
+    }
+
+  }
+
+  namespace test_digit_separators
+  {
+
+    constexpr auto ten_million = 100'000'000;
+    static_assert(ten_million == 100000000, "");
+
+  }
+
+  namespace test_return_type_deduction
+  {
+
+    auto f(int& x) { return x; }
+    decltype(auto) g(int& x) { return x; }
+
+    template < typename T1, typename T2 >
+    struct is_same
+    {
+      static constexpr auto value = false;
+    };
+
+    template < typename T >
+    struct is_same<T, T>
+    {
+      static constexpr auto value = true;
+    };
+
+    int
+    test()
+    {
+      auto x = 0;
+      static_assert(is_same<int, decltype(f(x))>::value, "");
+      static_assert(is_same<int&, decltype(g(x))>::value, "");
+      return x;
+    }
+
+  }
+
+}  // namespace cxx14
+
+#endif  // __cplusplus >= 201402L
+
+]])
+
+
+dnl  Tests for new features in C++17
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_17], [[
+
+// If the compiler admits that it is not ready for C++17, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201703L
+
+#error "This is not a C++17 compiler"
+
+#else
+
+#include <initializer_list>
+#include <utility>
+#include <type_traits>
+
+namespace cxx17
+{
+
+  namespace test_constexpr_lambdas
+  {
+
+    constexpr int foo = [](){return 42;}();
+
+  }
+
+  namespace test::nested_namespace::definitions
+  {
+
+  }
+
+  namespace test_fold_expression
+  {
+
+    template<typename... Args>
+    int multiply(Args... args)
+    {
+      return (args * ... * 1);
+    }
+
+    template<typename... Args>
+    bool all(Args... args)
+    {
+      return (args && ...);
+    }
+
+  }
+
+  namespace test_extended_static_assert
+  {
+
+    static_assert (true);
+
+  }
+
+  namespace test_auto_brace_init_list
+  {
+
+    auto foo = {5};
+    auto bar {5};
+
+    static_assert(std::is_same<std::initializer_list<int>, decltype(foo)>::value);
+    static_assert(std::is_same<int, decltype(bar)>::value);
+  }
+
+  namespace test_typename_in_template_template_parameter
+  {
+
+    template<template<typename> typename X> struct D;
+
+  }
+
+  namespace test_fallthrough_nodiscard_maybe_unused_attributes
+  {
+
+    int f1()
+    {
+      return 42;
+    }
+
+    [[nodiscard]] int f2()
+    {
+      [[maybe_unused]] auto unused = f1();
+
+      switch (f1())
+      {
+      case 17:
+        f1();
+        [[fallthrough]];
+      case 42:
+        f1();
+      }
+      return f1();
+    }
+
+  }
+
+  namespace test_extended_aggregate_initialization
+  {
+
+    struct base1
+    {
+      int b1, b2 = 42;
+    };
+
+    struct base2
+    {
+      base2() {
+        b3 = 42;
+      }
+      int b3;
+    };
+
+    struct derived : base1, base2
+    {
+        int d;
+    };
+
+    derived d1 {{1, 2}, {}, 4};  // full initialization
+    derived d2 {{}, {}, 4};      // value-initialized bases
+
+  }
+
+  namespace test_general_range_based_for_loop
+  {
+
+    struct iter
+    {
+      int i;
+
+      int& operator* ()
+      {
+        return i;
+      }
+
+      const int& operator* () const
+      {
+        return i;
+      }
+
+      iter& operator++()
+      {
+        ++i;
+        return *this;
+      }
+    };
+
+    struct sentinel
+    {
+      int i;
+    };
+
+    bool operator== (const iter& i, const sentinel& s)
+    {
+      return i.i == s.i;
+    }
+
+    bool operator!= (const iter& i, const sentinel& s)
+    {
+      return !(i == s);
+    }
+
+    struct range
+    {
+      iter begin() const
+      {
+        return {0};
+      }
+
+      sentinel end() const
+      {
+        return {5};
+      }
+    };
+
+    void f()
+    {
+      range r {};
+
+      for (auto i : r)
+      {
+        [[maybe_unused]] auto v = i;
+      }
+    }
+
+  }
+
+  namespace test_lambda_capture_asterisk_this_by_value
+  {
+
+    struct t
+    {
+      int i;
+      int foo()
+      {
+        return [*this]()
+        {
+          return i;
+        }();
+      }
+    };
+
+  }
+
+  namespace test_enum_class_construction
+  {
+
+    enum class byte : unsigned char
+    {};
+
+    byte foo {42};
+
+  }
+
+  namespace test_constexpr_if
+  {
+
+    template <bool cond>
+    int f ()
+    {
+      if constexpr(cond)
+      {
+        return 13;
+      }
+      else
+      {
+        return 42;
+      }
+    }
+
+  }
+
+  namespace test_selection_statement_with_initializer
+  {
+
+    int f()
+    {
+      return 13;
+    }
+
+    int f2()
+    {
+      if (auto i = f(); i > 0)
+      {
+        return 3;
+      }
+
+      switch (auto i = f(); i + 4)
+      {
+      case 17:
+        return 2;
+
+      default:
+        return 1;
+      }
+    }
+
+  }
+
+  namespace test_template_argument_deduction_for_class_templates
+  {
+
+    template <typename T1, typename T2>
+    struct pair
+    {
+      pair (T1 p1, T2 p2)
+        : m1 {p1},
+          m2 {p2}
+      {}
+
+      T1 m1;
+      T2 m2;
+    };
+
+    void f()
+    {
+      [[maybe_unused]] auto p = pair{13, 42u};
+    }
+
+  }
+
+  namespace test_non_type_auto_template_parameters
+  {
+
+    template <auto n>
+    struct B
+    {};
+
+    B<5> b1;
+    B<'a'> b2;
+
+  }
+
+  namespace test_structured_bindings
+  {
+
+    int arr[2] = { 1, 2 };
+    std::pair<int, int> pr = { 1, 2 };
+
+    auto f1() -> int(&)[2]
+    {
+      return arr;
+    }
+
+    auto f2() -> std::pair<int, int>&
+    {
+      return pr;
+    }
+
+    struct S
+    {
+      int x1 : 2;
+      volatile double y1;
+    };
+
+    S f3()
+    {
+      return {};
+    }
+
+    auto [ x1, y1 ] = f1();
+    auto& [ xr1, yr1 ] = f1();
+    auto [ x2, y2 ] = f2();
+    auto& [ xr2, yr2 ] = f2();
+    const auto [ x3, y3 ] = f3();
+
+  }
+
+  namespace test_exception_spec_type_system
+  {
+
+    struct Good {};
+    struct Bad {};
+
+    void g1() noexcept;
+    void g2();
+
+    template<typename T>
+    Bad
+    f(T*, T*);
+
+    template<typename T1, typename T2>
+    Good
+    f(T1*, T2*);
+
+    static_assert (std::is_same_v<Good, decltype(f(g1, g2))>);
+
+  }
+
+  namespace test_inline_variables
+  {
+
+    template<class T> void f(T)
+    {}
+
+    template<class T> inline T g(T)
+    {
+      return T{};
+    }
+
+    template<> inline void f<>(int)
+    {}
+
+    template<> int g<>(int)
+    {
+      return 5;
+    }
+
+  }
+
+}  // namespace cxx17
+
+#endif  // __cplusplus < 201703L
+
+]])
diff --git a/rss/Makefile.am b/rss/Makefile.am
new file mode 100644 (file)
index 0000000..2602499
--- /dev/null
@@ -0,0 +1,126 @@
+AM_CPPFLAGS = \
+       -I$(builddir)/inc/ \
+       -I$(srcdir)/inc/
+
+bin_PROGRAMS = rss-create
+
+rss_create_SOURCES = \
+       src/add.c \
+       src/default.c \
+       src/escape.c \
+       src/info.c \
+       src/log.c \
+       src/main.c \
+       src/meta.c \
+       src/next.c \
+       src/opt/desc.c \
+       src/opt/feed.c \
+       src/opt/link.c \
+       src/opt/meta.c \
+       src/opt/name.c \
+       src/opt/out.c \
+       src/util/now.c
+
+rss_create_SOURCES += \
+       inc/add.h \
+       inc/default.h \
+       inc/escape.h \
+       inc/info.h \
+       inc/log.h \
+       inc/main.h \
+       inc/meta.h \
+       inc/next.h \
+       inc/opt.h \
+       inc/util.h
+
+EXTRA_DIST = \
+       test/add.tests.h \
+       test/escape.tests.h \
+       test/info.tests.h \
+       test/meta.tests.h \
+       test/next.tests.h \
+       test/opt.desc.tests.h \
+       test/opt.feed.tests.h \
+       test/opt.link.tests.h \
+       test/opt.meta.tests.h \
+       test/opt.name.tests.h \
+       test/opt.out.tests.h \
+       test/util.now.tests.h \
+       test/test_utils.h
+
+check_PROGRAMS = add_tests escape_tests info_tests meta_tests next_tests opt_description_tests opt_feed_tests opt_link_tests opt_meta_tests opt_name_tests opt_out_tests util_now_tests
+
+TESTS = $(check_PROGRAMS)
+
+if ENABLE_MEMCHECK
+LOG_COMPILER = $(VALGRIND)
+AM_LOG_FLAGS = --leak-check=full -v --track-origins=yes --error-exitcode=1
+endif
+
+common_SOURCES = \
+       src/default.c \
+       src/log.c \
+       src/opt/out.c \
+       src/util/now.c \
+       test/test_utils.c
+
+add_tests_SOURCES = \
+       $(common_SOURCES) \
+       src/escape.c \
+       src/meta.c \
+       test/add.tests.c
+
+escape_tests_SOURCES = \
+       $(common_SOURCES) \
+       src/escape.c \
+       test/escape.tests.c
+
+info_tests_SOURCES = \
+       $(common_SOURCES) \
+       src/info.c \
+       test/info.tests.c
+
+meta_tests_SOURCES = \
+       $(common_SOURCES) \
+       src/escape.c \
+       src/meta.c \
+       test/meta.tests.c
+
+next_tests_SOURCES = \
+       $(common_SOURCES) \
+       test/next.tests.c
+
+opt_description_tests_SOURCES = \
+       $(common_SOURCES) \
+       src/opt/desc.c \
+       test/opt.desc.tests.c
+
+opt_feed_tests_SOURCES = \
+       $(common_SOURCES) \
+       src/opt/feed.c \
+       test/opt.feed.tests.c
+
+opt_link_tests_SOURCES = \
+       $(common_SOURCES) \
+       src/opt/link.c \
+       test/opt.link.tests.c
+
+opt_meta_tests_SOURCES = \
+       $(common_SOURCES) \
+       src/escape.c \
+       src/meta.c \
+       src/opt/meta.c \
+       test/opt.meta.tests.c
+
+opt_name_tests_SOURCES = \
+       $(common_SOURCES) \
+       src/opt/name.c \
+       test/opt.name.tests.c
+
+opt_out_tests_SOURCES = \
+       $(common_SOURCES) \
+       test/opt.out.tests.c
+
+util_now_tests_SOURCES = \
+       $(common_SOURCES) \
+       test/util.now.tests.c
diff --git a/rss/inc/add.h b/rss/inc/add.h
new file mode 100644 (file)
index 0000000..2324c22
--- /dev/null
@@ -0,0 +1,25 @@
+#ifndef __ADD_H_
+#define __ADD_H_
+
+#include<stdio.h>
+
+#include<log.h>
+#include<meta.h>
+#include<next.h>
+#include<opt.h>
+#include<rss.h>
+#include<util.h>
+
+#define FIELD_ADDER(x) add_field(x,FIELD_PREFIX(FIELD_STRING_LOADER(x)),FIELD_SUFFIX(FIELD_STRING_LOADER(x)))
+
+#define add_title() FIELD_ADDER(FIELD_TITLE)
+#define add_link() FIELD_ADDER(FIELD_LINK)
+#define add_pubdate() FIELD_ADDER(FIELD_PUBDATE)
+#define add_description() FIELD_ADDER(FIELD_DESCRIPTION)
+#define add_guid() FIELD_ADDER(FIELD_GUID)
+
+int add();
+int add_default(enum field);
+int add_field(enum field,const char*,const char*);
+
+#endif
diff --git a/rss/inc/default.h b/rss/inc/default.h
new file mode 100644 (file)
index 0000000..4fe7fa9
--- /dev/null
@@ -0,0 +1,14 @@
+#ifndef __DEFAULT_H_
+#define __DEFAULT_H_
+
+#include<stdlib.h>
+#include<string.h>
+
+#include<log.h>
+#include<next.h>
+#include<opt.h>
+#include<util.h>
+
+int defaults();
+
+#endif
diff --git a/rss/inc/escape.h b/rss/inc/escape.h
new file mode 100644 (file)
index 0000000..8b6f24a
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef __ESCAPE_H_
+#define __ESCAPE_H_
+
+#include<stdlib.h>
+#include<string.h>
+
+#include<log.h>
+
+int escape(char*,size_t);
+
+#endif
diff --git a/rss/inc/info.h b/rss/inc/info.h
new file mode 100644 (file)
index 0000000..f6ed0cb
--- /dev/null
@@ -0,0 +1,13 @@
+#ifndef __INFO_H_
+#define __INFO_H_
+
+#include<log.h>
+#include<opt.h>
+#include<util.h>
+
+#define INFO_DEFAULT_DESCRIPTION "(no description)"
+#define INFO_DEFAULT_LANGUAGE "en-us"
+
+int info();
+
+#endif
diff --git a/rss/inc/log.h b/rss/inc/log.h
new file mode 100644 (file)
index 0000000..dfd9c09
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef __LOG_H_
+#define __LOG_H_
+
+#include<stdarg.h>
+#include<stdio.h>
+
+extern FILE *output_stream;
+
+#define log_err(...) log_message(stderr,__VA_ARGS__)
+#define out(...) log_message(output_stream,__VA_ARGS__);
+
+void log_close();
+void log_message(FILE*,const char*,...);
+
+#endif
diff --git a/rss/inc/main.h b/rss/inc/main.h
new file mode 100644 (file)
index 0000000..504c00c
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef __MAIN_H_
+#define __MAIN_H_
+
+#include<getopt.h>
+#include<stdio.h>
+#include<stdlib.h>
+
+#include<add.h>
+#include<default.h>
+#include<info.h>
+#include<opt.h>
+
+int main(int,char**);
+
+#endif
diff --git a/rss/inc/meta.h b/rss/inc/meta.h
new file mode 100644 (file)
index 0000000..b43fc56
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef __META_H_
+#define __META_H_
+
+#include<string.h>
+
+#include<escape.h>
+#include<log.h>
+#include<next.h>
+#include<opt.h>
+#include<rss.h>
+
+int meta(char*,char[FIELD_COUNT][BUF_SIZE]);
+int meta_filename(char*,char*,size_t);
+
+#endif
diff --git a/rss/inc/next.h b/rss/inc/next.h
new file mode 100644 (file)
index 0000000..7125b6c
--- /dev/null
@@ -0,0 +1,10 @@
+#ifndef __NEXT_H_
+#define __NEXT_H_
+
+#include<stdio.h>
+
+#define BUF_SIZE 4096
+
+int next(char*,size_t);
+
+#endif
diff --git a/rss/inc/opt.h b/rss/inc/opt.h
new file mode 100644 (file)
index 0000000..bf000a9
--- /dev/null
@@ -0,0 +1,35 @@
+#ifndef __OPT_H_
+#define __OPT_H_
+
+#include<stddef.h>
+#include<stdio.h>
+#include<stdlib.h>
+#include<string.h>
+
+#include<log.h>
+#include<meta.h>
+#include<rss.h>
+
+struct channel_options {
+       char *title;
+       char *link;
+       char *description;
+       char *language;
+       char *last_build_date;
+};
+
+struct global_options {
+       struct channel_options channel;
+       char *feedurl;
+};
+
+extern struct global_options global_opts;
+
+int opt_set_and_parse_meta(char*);
+int opt_set_description(char*);
+int opt_set_feedurl(char*);
+int opt_set_link(char*);
+int opt_set_name(char*);
+int opt_set_output(char*);
+
+#endif
diff --git a/rss/inc/rss.h b/rss/inc/rss.h
new file mode 100644 (file)
index 0000000..97c20cd
--- /dev/null
@@ -0,0 +1,29 @@
+#ifndef __RSS_H_
+#define __RSS_H_
+
+enum field {
+       FIELD_TITLE = 0,
+       FIELD_LINK,
+       FIELD_PUBDATE,
+       FIELD_DESCRIPTION,
+       FIELD_GUID,
+       FIELD_LANGUAGE,
+       FIELD_COUNT
+};
+
+#define XML_START "<"
+#define XML_CLOSE "</"
+#define XML_END ">"
+
+#define FIELD_TITLE_STRING "title"
+#define FIELD_LINK_STRING "link"
+#define FIELD_PUBDATE_STRING "pubDate"
+#define FIELD_DESCRIPTION_STRING "description"
+#define FIELD_GUID_STRING "guid"
+#define FIELD_LANGUAGE_STRING "language"
+
+#define FIELD_STRING_LOADER(x) x ## _STRING
+#define FIELD_PREFIX(x) XML_START x XML_END
+#define FIELD_SUFFIX(x) XML_CLOSE x XML_END
+
+#endif
diff --git a/rss/inc/util.h b/rss/inc/util.h
new file mode 100644 (file)
index 0000000..448c56d
--- /dev/null
@@ -0,0 +1,9 @@
+#ifndef __UTIL_H_
+#define __UTIL_H_
+
+#include<stdlib.h>
+#include<time.h>
+
+int now_string(char**);
+
+#endif
diff --git a/rss/src/add.c b/rss/src/add.c
new file mode 100644 (file)
index 0000000..e0b9e2d
--- /dev/null
@@ -0,0 +1,65 @@
+#include<add.h>
+
+char bufs[FIELD_COUNT][BUF_SIZE] = {0};
+
+int add() {
+       int i;
+
+       // clear bufs
+       for(i=0;i<FIELD_COUNT;i++) {
+               bufs[i][0] = '\0';
+       }
+
+       // place next file into FIELD_TITLE buffer
+       if((i = next(bufs[FIELD_TITLE],BUF_SIZE))<0) { return -1; }
+       if(i==0) { return 0; }
+
+       // try to get metadata
+       if(meta(NULL,bufs)<0) { return -1; }
+
+       out("<item>");
+       if(add_title()<0) { return -1; }
+       if(add_link()<0) { return -1; }
+       if(add_pubdate()<0) { return -1; }
+       if(add_description()<0) { return -1; }
+       if(add_guid()<0) { return -1; }
+       out("</item>");
+       return 1;
+}
+
+int add_field(enum field field, const char *prefix, const char *suffix) {
+       if(strlen(bufs[field])>0) {
+               out("%s",prefix);
+               out("%s",bufs[field]);
+               out("%s",suffix);
+       } else {
+               if(add_default(field)<0) { return -1; }
+       }
+
+       return 1;
+}
+
+int add_default(enum field type) {
+       char *timebuf;
+       switch(type) {
+               case FIELD_TITLE:
+               case FIELD_LINK:
+                       log_err("missing required field...aborting...\n");
+                       return -1;
+               case FIELD_DESCRIPTION:
+                       out("<description>no description</description>");
+                       break;
+               case FIELD_GUID:
+                       break;
+               case FIELD_PUBDATE:
+                       if(now_string(&timebuf)<0) { return -1; }
+                       out("<pubDate>%s</pubDate>",timebuf);
+                       free(timebuf);
+                       break;
+               default:
+                       log_err("unknown field type\n");
+                       return -1;
+       }
+
+       return 1;
+}
diff --git a/rss/src/default.c b/rss/src/default.c
new file mode 100644 (file)
index 0000000..860d6a3
--- /dev/null
@@ -0,0 +1,16 @@
+#include<default.h>
+
+struct global_options global_opts = { { NULL }, NULL };
+
+int defaults() {
+       struct channel_options *ch;
+
+       output_stream = stdout;
+       if(atexit(log_close)!=0) { return -1; }
+
+       ch = &(global_opts.channel);
+       
+       if(now_string(&(global_opts.channel.last_build_date))<0) { return -1; }
+
+       return 1;
+}
diff --git a/rss/src/escape.c b/rss/src/escape.c
new file mode 100644 (file)
index 0000000..2e7a3f9
--- /dev/null
@@ -0,0 +1,84 @@
+#include<escape.h>
+
+int escape(char *buf, size_t buf_size) {
+       char *p, *p2, *tmpbuf;
+       char c;
+       size_t len;
+       int left;
+
+       if(NULL==buf) { return -1; }
+       if(buf_size<=0) { return -1; }
+       if(strlen(buf)>buf_size) { return -1; }
+
+       tmpbuf = NULL;
+
+       char to_escape[] = "<\\>&";
+
+       p = strpbrk(buf,to_escape);
+       p2 = buf;
+       while(NULL!=p) {
+               if(NULL==tmpbuf) {
+                       tmpbuf = malloc(buf_size);
+                       if(NULL==tmpbuf) {
+                               log_err("allocation error\n");
+                               return -1;
+                       }
+                       tmpbuf[0] = '\0';
+               }
+
+               c = p[0];
+               p[0] = '\0';
+               p++;
+
+               strcat(tmpbuf,p2);
+
+               left = buf_size - strlen(tmpbuf);
+
+               switch(c) {
+                       case '<':
+                               if(left<=4) { goto panic; }
+                               strcat(tmpbuf,"&lt;");
+                               break;
+                       case '\\':
+                               if(p[0]=='n') {
+                                       if(left<1) { goto panic; }
+                                       strcat(tmpbuf,"\n");
+                                       p++;
+                               }
+                               break;
+                       case '>':
+                               if(left<=4) { goto panic; }
+                               strcat(tmpbuf,"&gt;");
+                               break;
+                       case '&':
+                               if(left<=5) { goto panic; }
+                               strcat(tmpbuf,"&amp;");
+                               break;
+                       default:
+                               log_err("unknown escape character: %c\n",c);
+                               goto panic;
+               }
+
+               p2 = p;
+               p = strpbrk(p,to_escape);
+       }
+
+       if(tmpbuf!=NULL) {
+               strcat(tmpbuf,p2);
+               len = strlen(tmpbuf);
+               if(len>buf_size) {
+                       log_err("escaped string larger than buffer...\n");
+                       goto panic;
+               }
+
+               strcpy(buf,tmpbuf);
+               free(tmpbuf);
+       }
+
+       return 1;
+       panic:
+               if(tmpbuf!=NULL) {
+                       free(tmpbuf);
+               }
+               return -1;
+}
diff --git a/rss/src/info.c b/rss/src/info.c
new file mode 100644 (file)
index 0000000..8dc9d5e
--- /dev/null
@@ -0,0 +1,36 @@
+#include<info.h>
+
+int info() {
+       struct channel_options *ch;
+
+       ch = &(global_opts.channel);
+
+       if(NULL==ch->title) {
+               log_err("missing channel title\n");
+               return -1;
+       }
+
+       if(NULL==ch->link) {
+               log_err("missing channel link\n");
+               return -1;
+       }
+
+       out("<title>%s</title>",global_opts.channel.title);
+       out("<link>%s</link>",global_opts.channel.link);
+
+       if(NULL==ch->description) {
+               out("<description>%s</description>",INFO_DEFAULT_DESCRIPTION);
+       } else {
+               out("<description>%s</description>",ch->description);
+       }
+
+       if(NULL==ch->language) {
+               out("<language>%s</language>",INFO_DEFAULT_LANGUAGE);
+       } else {
+               out("<language>%s</language>",global_opts.channel.language);
+       }
+
+       out("<lastBuildDate>%s</lastBuildDate>",global_opts.channel.last_build_date);
+
+       return 1;
+}
diff --git a/rss/src/log.c b/rss/src/log.c
new file mode 100644 (file)
index 0000000..1a6da1b
--- /dev/null
@@ -0,0 +1,16 @@
+#include<log.h>
+
+FILE *output_stream;
+
+void log_close() {
+       if(output_stream!=stdout) {
+               fclose(output_stream);
+       }
+}
+
+void log_message(FILE *out_stream, const char *format,...) {
+       va_list args;
+       va_start(args,format);
+       vfprintf(out_stream,format,args);
+       va_end(args);
+}
diff --git a/rss/src/main.c b/rss/src/main.c
new file mode 100644 (file)
index 0000000..cf82e47
--- /dev/null
@@ -0,0 +1,69 @@
+#include<main.h>
+
+static struct option long_options[] = {
+       {"description", required_argument, 0, 'd'},
+       {"feedurl", required_argument, 0, 'f'},
+       {"link", required_argument, 0, 'l'},
+       {"meta", required_argument, 0, 'm'},
+       {"name", required_argument, 0, 'n'},
+       {"output", required_argument, 0, 'o'},
+       {0,0,0,0}
+};
+
+int main(int argc, char **argv) {
+       int c,i;
+
+       if(defaults()<0) { return EXIT_FAILURE; }
+
+       while(1) {
+               int option_index = 0;
+
+               if((c = getopt_long(argc,argv,"d:f:l:m:n:o:",long_options,&option_index))==-1) { break; }
+               switch(c) {
+                       case 0:
+                               if(long_options[option_index].flag!=0) { break; }
+                               log_err("option %s", long_options[option_index].name);
+                               if(optarg) {
+                                       log_err(" with arg %s",optarg);
+                               }
+                               log_err("\n");
+                               return EXIT_FAILURE;
+
+                               break;
+                       case 'd':
+                               if(opt_set_description(optarg)<0) { return EXIT_FAILURE; }
+                               break;
+                       case 'f':
+                               if(opt_set_feedurl(optarg)<0) { return EXIT_FAILURE; }
+                               break;
+                       case 'l':
+                               if(opt_set_link(optarg)<0) { return EXIT_FAILURE; }
+                               break;
+                       case 'm':
+                               if(opt_set_and_parse_meta(optarg)<0) { return EXIT_FAILURE; }
+                               break;
+                       case 'n':
+                               if(opt_set_name(optarg)<0) { return EXIT_FAILURE; }
+                               break;
+                       case 'o':
+                               if(opt_set_output(optarg)<0) { return EXIT_FAILURE; }
+                               break;
+                       case '?':
+                       default:
+                               return EXIT_FAILURE;
+               }
+       }
+
+       out("<rss version=\"2.0\">");
+       out("<channel>");
+
+       if(info()<0) { return EXIT_FAILURE; }
+
+       while((i = add())>0) { }
+
+       if(i<0) { return EXIT_FAILURE; }
+
+       out("</channel></rss>\n");
+
+       return EXIT_SUCCESS;
+}
diff --git a/rss/src/meta.c b/rss/src/meta.c
new file mode 100644 (file)
index 0000000..9cb2bdc
--- /dev/null
@@ -0,0 +1,100 @@
+#include<meta.h>
+
+int meta_filename(char *file, char *out, size_t out_len) {
+       char *p;
+       size_t len;
+
+       p = strrchr(file,'/');
+       if(NULL==p) {
+               p = file;
+       } else {
+               p++;
+       }
+
+       len = p-file;
+       // prefix length, "." [1], suffix length, ".meta", '\0'
+       if((len+1+strlen(p)+5+1)>out_len) {
+               return -1;
+       }
+
+       if(len>0) {
+               strncpy(out,file,len);
+       }
+
+       out[len] = '.';
+       out[len+1] = '\0';
+       strcat(out,p);
+       strcat(out,".meta");
+
+       return 1;
+}
+
+int meta(char *meta_file, char bufs[FIELD_COUNT][BUF_SIZE]) {
+       char tmp[BUF_SIZE];
+       char *p,*p2;
+       FILE *fp;
+       int i;
+
+       if(NULL==meta_file) {
+               if(meta_filename(bufs[FIELD_TITLE],tmp,BUF_SIZE)<0) { return -1; }
+               fp = fopen(tmp,"r");
+       } else {
+               fp = fopen(meta_file,"r");
+               bufs[FIELD_TITLE][0] = '\0';
+       }
+
+       if(NULL==fp) { return 0; }
+
+       // FIELD_TITLE=0
+       for(i=1;i<FIELD_COUNT;i++) {
+               bufs[i][0] = '\0';
+       }
+
+       while(fgets(tmp,BUF_SIZE,fp)!=NULL) {
+               p = strchr(tmp,'=');
+               if(NULL==p) { goto panic; }
+               
+               *p = '\0';
+               p++;
+
+               p2 = p;
+               while(*p2!='\0') {
+                       if('\n'==*p2) {
+                               *p2 = '\0';
+                               break;
+                       }
+                       p2++;
+               }
+
+               if(escape(p,BUF_SIZE-(p-tmp))<0) { goto panic; }
+
+               if(strcmp(tmp,FIELD_TITLE_STRING)==0) {
+                       strcpy(bufs[FIELD_TITLE],p);
+               } else if(strcmp(tmp,FIELD_LINK_STRING)==0) {
+                       if(NULL!=global_opts.feedurl) {
+                               strcpy(bufs[FIELD_LINK],global_opts.feedurl);
+                               strcat(bufs[FIELD_LINK],p);
+                       } else {
+                               strcpy(bufs[FIELD_LINK],p);
+                       }
+               } else if(strcmp(tmp,FIELD_PUBDATE_STRING)==0) {
+                       strcpy(bufs[FIELD_PUBDATE],p);
+               } else if(strcmp(tmp,FIELD_DESCRIPTION_STRING)==0) {
+                       strcpy(bufs[FIELD_DESCRIPTION],p);
+               } else if(strcmp(tmp,FIELD_GUID_STRING)==0) {
+                       strcpy(bufs[FIELD_GUID],p);
+               } else if(strcmp(tmp,FIELD_LANGUAGE_STRING)==0) {
+                       strcpy(bufs[FIELD_LANGUAGE],p);
+               } else {
+                       log_err("unknown field: %s\n",tmp);
+                       goto panic;
+               }
+       }
+
+       fclose(fp);
+       return 1;
+       panic:
+               log_err("malformed .meta file: %s\n", meta_file);
+               fclose(fp);
+               return -1;
+}
diff --git a/rss/src/next.c b/rss/src/next.c
new file mode 100644 (file)
index 0000000..c1067b6
--- /dev/null
@@ -0,0 +1,18 @@
+#include<next.h>
+
+int next(char *buf, size_t buf_size) {
+       int i;
+       
+       if(NULL==buf) { return -1; }
+
+       i = 0;
+       while((buf[i] = getchar())!=EOF) {
+               if(buf[i]=='\n') { break; }
+               if(i>=buf_size) { return -1; }
+               i++;
+       }
+
+       buf[i] = '\0';
+
+       return (i>0);
+}
diff --git a/rss/src/opt/desc.c b/rss/src/opt/desc.c
new file mode 100644 (file)
index 0000000..00129df
--- /dev/null
@@ -0,0 +1,19 @@
+#include<opt.h>
+
+int opt_set_description(char *desc) {
+       char *p;
+       size_t len;
+
+       if(NULL==desc) { return -1; }
+
+       len = strlen(desc);
+       if(len==0) { return -1; }
+
+       p = malloc(sizeof(char)*(len+1));
+       if(NULL==p) { return -1; }
+
+       strcpy(p,desc);
+       global_opts.channel.description = p;
+
+       return 1;
+}
diff --git a/rss/src/opt/feed.c b/rss/src/opt/feed.c
new file mode 100644 (file)
index 0000000..321e084
--- /dev/null
@@ -0,0 +1,26 @@
+#include<opt.h>
+
+int opt_set_feedurl(char *url) {
+       char *p;
+       size_t len;
+
+       if(NULL==url) { return -1; }
+
+       len = strlen(url);
+       if(len==0) { return -1; }
+
+       if(url[len-1]!='/') { len++; }
+
+       p = malloc(sizeof(char)*(len+1));
+       if(NULL==p) { return -1; }
+
+       strcpy(p,url);
+
+       if(url[len-1]!='/') {
+               strcat(p,"/");
+       }
+
+       global_opts.feedurl = p;
+
+       return 1;
+}
diff --git a/rss/src/opt/link.c b/rss/src/opt/link.c
new file mode 100644 (file)
index 0000000..8d2d4e1
--- /dev/null
@@ -0,0 +1,19 @@
+#include<opt.h>
+
+int opt_set_link(char *link) {
+       char *p;
+       size_t len;
+
+       if(NULL==link) { return -1; }
+
+       len = strlen(link);
+       if(len==0) { return -1; }
+
+       p = malloc(sizeof(char)*(len+1));
+       if(NULL==p) { return -1; }
+
+       strcpy(p,link);
+       global_opts.channel.link = p;
+
+       return 1;
+}
diff --git a/rss/src/opt/meta.c b/rss/src/opt/meta.c
new file mode 100644 (file)
index 0000000..cc7e21d
--- /dev/null
@@ -0,0 +1,74 @@
+#include<opt.h>
+
+int opt_set_and_parse_meta(char *filename) {
+       char bufs[FIELD_COUNT][BUF_SIZE] = {0};
+       char *p;
+       size_t len, title_len, desc_len, link_len, guid_len, lan_len;
+       struct channel_options *ch;
+       int i;
+
+       if(NULL==filename) { return -1; }
+
+       if((i = meta(filename,bufs))<0) { return -1; }
+       if(i==0) {
+               log_err("missing meta file: %s\n",filename);
+               return -1;
+       }
+
+       ch = &(global_opts.channel);
+
+       title_len = strlen(bufs[FIELD_TITLE]);
+       desc_len = strlen(bufs[FIELD_DESCRIPTION]);
+       link_len = strlen(bufs[FIELD_LINK]);
+       guid_len = strlen(bufs[FIELD_GUID]);
+       lan_len = strlen(bufs[FIELD_LANGUAGE]);
+       len = title_len+desc_len+link_len+guid_len+lan_len+5;
+       
+       if(len>0) {
+               p = malloc(sizeof(char)*(len));
+               if(NULL==p) { return -1; }
+       }
+
+       if(title_len>0) {
+               ch->title = p;
+               strcpy(ch->title,bufs[FIELD_TITLE]);
+               p += title_len+1;
+       }
+
+       if(desc_len>0) {
+               ch->description = p;
+               strcpy(ch->description,bufs[FIELD_DESCRIPTION]);
+               p += desc_len+1;
+       }
+
+       if(link_len>0) {
+               if(NULL==ch->description) {
+                       ch->description = p;
+                       ch->description[0] = '\0';
+               } else {
+                       strcat(ch->description,"\n");
+               }
+
+               strcat(ch->description,bufs[FIELD_LINK]);
+               p += link_len+1;
+       }
+
+       if(guid_len>0) {
+               if(NULL==ch->description) {
+                       ch->description = p;
+                       ch->description[0] = '\0';
+               } else {
+                       strcat(ch->description,"\n");
+               }
+
+               strcat(ch->description,bufs[FIELD_GUID]);
+               p += guid_len+1;
+       }
+
+       if(lan_len>0) {
+               ch->language = p;
+               strcpy(ch->language,bufs[FIELD_LANGUAGE]);
+       }
+
+       return 1;
+}
diff --git a/rss/src/opt/name.c b/rss/src/opt/name.c
new file mode 100644 (file)
index 0000000..9883836
--- /dev/null
@@ -0,0 +1,19 @@
+#include<opt.h>
+
+int opt_set_name(char *name) {
+       char *p;
+       size_t len;
+
+       if(NULL==name) { return -1; }
+
+       len = strlen(name);
+       if(len==0) { return -1; }
+
+       p = malloc(sizeof(char)*(len+1));
+       if(NULL==p) { return -1; }
+
+       strcpy(p,name);
+       global_opts.channel.title = p;
+
+       return 1;
+}
diff --git a/rss/src/opt/out.c b/rss/src/opt/out.c
new file mode 100644 (file)
index 0000000..7d1956d
--- /dev/null
@@ -0,0 +1,16 @@
+#include<opt.h>
+
+int opt_set_output(char *target) {
+       FILE *fp;
+
+       if(NULL==target) { return -1; }
+
+       fp = fopen(target,"w");
+       if(NULL==fp) {
+               log_err("unable to open %s for writing\n",target);
+               return -1;
+       }
+
+       output_stream = fp;
+       return 1;
+}
diff --git a/rss/src/util/now.c b/rss/src/util/now.c
new file mode 100644 (file)
index 0000000..b761e07
--- /dev/null
@@ -0,0 +1,15 @@
+#include<util.h>
+
+int now_string(char **timebuf) {
+       if(NULL==timebuf) { return -1; }
+
+       *timebuf = malloc(sizeof(char)*70);
+       if(NULL==(*timebuf)) { return -1; }
+
+       struct tm now;
+
+       now = *localtime(&(time_t){time(NULL)});
+       if(!(strftime((*timebuf),sizeof(char)*70, "%a, %d %b %Y %H:%M:%S %z", &now))) { return -1; }
+
+       return 1;
+}
diff --git a/rss/test/add.tests.c b/rss/test/add.tests.c
new file mode 100644 (file)
index 0000000..513fbb0
--- /dev/null
@@ -0,0 +1,52 @@
+#include "add.tests.h"
+
+int main() {
+       setup_env();
+
+       add_basic_tests();
+
+       clean_env();
+
+       return EXIT_SUCCESS;
+}
+
+static int i = 0;
+
+void add_basic_tests() {
+       FILE *fp;
+       char buf[1024];
+
+       assert(opt_set_output(DATADIR "feed.xml")==1);
+       
+       assert(add()==1);
+       assert(add()==-1);
+
+       log_close();
+
+       fp = fopen(DATADIR "feed.xml","r");
+       assert(NULL!=fp);
+
+       assert(fread(buf,sizeof(char),1024,fp)==260);
+       assert(fclose(fp)==0);
+
+       assert(memcmp(buf,"<item><title>/tmp/rss-create/test/test2.txt</title><link>0bc6c0de1ff1d26d84187a4d42fb9a2d0bc73a1d.torrent</link><pubDate>Sat, 15 May 2021 11:01:04 +0000</pubDate><description>no description</description></item><item><title>/tmp/rss-create/test/test.txt</title>",260)==0);
+}
+
+int next_dummy(char *buf, size_t size) {
+       switch(i) {
+               case 0:
+                       snprintf(buf,size,FILE_2);
+                       break;
+               case 1:
+                       snprintf(buf,size,FILE_1);
+                       break;
+               case 2:
+                       snprintf(buf,size,FILE_3);
+                       break;
+               default:
+                       snprintf(buf,size,"notarealfile");
+                       break;
+       }
+       i++;
+       return 1;
+}
diff --git a/rss/test/add.tests.h b/rss/test/add.tests.h
new file mode 100644 (file)
index 0000000..d3985f7
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef __ADD_TESTS_H_
+#define __ADD_TESTS_H_
+
+#include "test_utils.h"
+
+#include<add.h>
+
+int main();
+void add_basic_tests();
+int next_dummy(char*,size_t);
+
+#define next(buf,size) next_dummy(buf,size)
+#include "../src/add.c"
+
+#endif
diff --git a/rss/test/escape.tests.c b/rss/test/escape.tests.c
new file mode 100644 (file)
index 0000000..83de5c1
--- /dev/null
@@ -0,0 +1,55 @@
+#include "escape.tests.h"
+
+int main() {
+       setup_env();
+
+       escape_basic_tests();
+
+       clean_env();
+
+       return EXIT_SUCCESS;
+}
+
+#define BUF_SIZE 1024
+void escape_basic_tests() {
+       char buf[BUF_SIZE];
+       
+       assert(escape(NULL,0)==-1);
+       assert(escape(buf,0)==-1);
+
+       strcpy(buf,"<");
+       assert(escape(buf,BUF_SIZE)==1);
+       assert(strcmp(buf,"&lt;")==0);
+
+       strcpy(buf,">");
+       assert(escape(buf,BUF_SIZE)==1);
+       assert(strcmp(buf,"&gt;")==0);
+
+       strcpy(buf,"\\n");
+       assert(escape(buf,BUF_SIZE)==1);
+       assert(strcmp(buf,"\n")==0);
+
+       strcpy(buf,"&");
+       assert(escape(buf,BUF_SIZE)==1);
+       assert(strcmp(buf,"&amp;")==0);
+
+       strcpy(buf,"this");
+       assert(escape(buf,6)==1);
+
+       strcpy(buf,"this&");
+       assert(escape(buf,6)==-1);
+
+       strcpy(buf,"this<");
+       assert(escape(buf,6)==-1);
+
+       strcpy(buf,"this>");
+       assert(escape(buf,6)==-1);
+
+       strcpy(buf,"this\\n");
+       assert(escape(buf,4)==-1);
+
+       strcpy(buf,"lskjdflaksjdfl<>slkdfjlsdf&lsdjkfsldfjsldkfj\\nsldkjfslkdjfk<<<<>>>>");
+       assert(escape(buf,10)==-1);
+       assert(escape(buf,1024)==1);
+       assert(strcmp(buf,"lskjdflaksjdfl&lt;&gt;slkdfjlsdf&amp;lsdjkfsldfjsldkfj\nsldkjfslkdjfk&lt;&lt;&lt;&lt;&gt;&gt;&gt;&gt;")==0);
+}
diff --git a/rss/test/escape.tests.h b/rss/test/escape.tests.h
new file mode 100644 (file)
index 0000000..d2eb9b3
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef __ESCAPE_TESTS_H_
+#define __ESCAPE_TESTS_H_
+
+#include "test_utils.h"
+
+#include<escape.h>
+
+int main();
+void escape_basic_tests();
+
+#endif
diff --git a/rss/test/info.tests.c b/rss/test/info.tests.c
new file mode 100644 (file)
index 0000000..9dafb6b
--- /dev/null
@@ -0,0 +1,41 @@
+#include "info.tests.h"
+
+int main() {
+       setup_env();
+
+       info_basic_tests();
+
+       clean_env();
+
+       return EXIT_SUCCESS;
+}
+
+void info_basic_tests() {
+       FILE *fp;
+       char buf[1024];
+       char buf2[1024];
+
+       assert(opt_set_output(DATADIR "feed.xml")==1);
+
+       assert(info()==-1);
+
+       global_opts.channel.title = "test title";
+       assert(info()==-1);
+
+       global_opts.channel.link = "https://test.com/test/feed.xml";
+       assert(now_string(&(global_opts.channel.last_build_date))==1);
+
+       assert(info()==1);
+
+       log_close();
+
+       fp = fopen(DATADIR "feed.xml","r");
+       assert(NULL!=fp);
+
+       assert(fread(buf,sizeof(char),1024,fp)==199);
+       assert(fclose(fp)==0);
+
+       sprintf(buf2,"<title>test title</title><link>https://test.com/test/feed.xml</link><description>(no description)</description><language>en-us</language><lastBuildDate>%s</lastBuildDate>",global_opts.channel.last_build_date);
+
+       assert(memcmp(buf,buf2,199)==0);
+}
diff --git a/rss/test/info.tests.h b/rss/test/info.tests.h
new file mode 100644 (file)
index 0000000..8fbc311
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef __INFO_TESTS_H_
+#define __INFO_TESTS_H_
+
+#include "test_utils.h"
+
+#include<info.h>
+
+int main();
+void info_basic_tests();
+
+#endif
diff --git a/rss/test/meta.tests.c b/rss/test/meta.tests.c
new file mode 100644 (file)
index 0000000..05f3b8e
--- /dev/null
@@ -0,0 +1,219 @@
+#include "meta.tests.h"
+
+int main() {
+       setup_env();
+
+       meta_basic_tests();
+       meta_feedurl_test();
+       meta_filename_basic_test();
+       meta_duplicate_field_test();
+
+       clean_env();
+
+       return EXIT_SUCCESS;
+}
+
+void meta_basic_tests() {
+       char bufs[FIELD_COUNT][BUF_SIZE];
+
+       assert(meta("nonexistentfile",bufs)==0);
+       strcpy(bufs[FIELD_TITLE],"nonexistentfile");
+       assert(meta(NULL,bufs)==0);
+
+       assert(meta(FILE_1,bufs)==-1);
+       strcpy(bufs[FIELD_TITLE],FILE_1);
+       assert(meta(NULL,bufs)==0);
+
+       assert(meta(FILE_2,bufs)==-1);
+       strcpy(bufs[FIELD_TITLE],FILE_2);
+       assert(meta(NULL,bufs)==1);
+       assert(strcmp(bufs[FIELD_TITLE],FILE_2)==0);
+       assert(strcmp(bufs[FIELD_LINK],"0bc6c0de1ff1d26d84187a4d42fb9a2d0bc73a1d.torrent")==0);
+       assert(strcmp(bufs[FIELD_PUBDATE],"Sat, 15 May 2021 11:01:04 +0000")==0);
+       assert(bufs[FIELD_DESCRIPTION][0]=='\0');
+       assert(bufs[FIELD_GUID][0]=='\0');
+       assert(bufs[FIELD_LANGUAGE][0]=='\0');
+
+       assert(meta(FILE_3,bufs)==-1);
+       strcpy(bufs[FIELD_TITLE],FILE_3);
+       assert(meta(NULL,bufs)==0);
+
+       assert(meta(FILE_4,bufs)==1);
+       assert(bufs[FIELD_TITLE][0]=='\0');
+       assert(bufs[FIELD_LINK][0]=='\0');
+       assert(strcmp(bufs[FIELD_PUBDATE],"Sun, 16 May 2021 02:55:07 +0000")==0);
+       assert(strcmp(bufs[FIELD_DESCRIPTION],"this a&lt;Mactual&gt;ly show\na new line")==0);
+       assert(bufs[FIELD_GUID][0]=='\0');
+       assert(bufs[FIELD_LANGUAGE][0]=='\0');
+       strcpy(bufs[FIELD_TITLE],FILE_4);
+       assert(meta(NULL,bufs)==0);
+
+       assert(meta(FILE_5,bufs)==1);
+       assert(bufs[FIELD_TITLE][0]=='\0');
+       assert(strcmp(bufs[FIELD_LINK],"0bc6c0de1ff1d26d84187a4d42fb9a2d0bc73a1d.torrent")==0);
+       assert(strcmp(bufs[FIELD_PUBDATE],"Sat, 15 May 2021 11:01:04 +0000")==0);
+       assert(bufs[FIELD_DESCRIPTION][0]=='\0');
+       assert(bufs[FIELD_GUID][0]=='\0');
+       assert(bufs[FIELD_LANGUAGE][0]=='\0');
+       strcpy(bufs[FIELD_TITLE],FILE_5);
+       assert(meta(NULL,bufs)==0);
+
+       assert(meta(FILE_6,bufs)==-1);
+       strcpy(bufs[FIELD_TITLE],FILE_6);
+       assert(meta(NULL,bufs)==0);
+
+       assert(meta(FILE_7,bufs)==1);
+       assert(bufs[FIELD_TITLE][0]=='\0');
+       assert(bufs[FIELD_LINK][0]=='\0');
+       assert(bufs[FIELD_PUBDATE][0]=='\0');
+       assert(bufs[FIELD_DESCRIPTION][0]=='\0');
+       assert(strcmp(bufs[FIELD_GUID],"hello")==0);
+       assert(bufs[FIELD_LANGUAGE][0]=='\0');
+       strcpy(bufs[FIELD_TITLE],FILE_7);
+       assert(meta(NULL,bufs)==0);
+
+       assert(meta(FILE_8,bufs)==-1);
+       strcpy(bufs[FIELD_TITLE],FILE_8);
+       assert(meta(NULL,bufs)==1);
+       assert(strcmp(bufs[FIELD_TITLE],FILE_8)==0);
+       assert(bufs[FIELD_LINK][0]=='\0');
+       assert(bufs[FIELD_PUBDATE][0]=='\0');
+       assert(bufs[FIELD_DESCRIPTION][0]=='\0');
+       assert(strcmp(bufs[FIELD_GUID],"hello")==0);
+       assert(bufs[FIELD_LANGUAGE][0]=='\0');
+
+       assert(meta(FILE_9,bufs)==1);
+       assert(bufs[FIELD_TITLE][0]=='\0');
+       assert(bufs[FIELD_LINK][0]=='\0');
+       assert(bufs[FIELD_PUBDATE][0]=='\0');
+       assert(strcmp(bufs[FIELD_DESCRIPTION],"this is a test description")==0);
+       assert(bufs[FIELD_GUID][0]=='\0');
+       assert(bufs[FIELD_LANGUAGE][0]=='\0');
+       strcpy(bufs[FIELD_TITLE],FILE_9);
+       assert(meta(NULL,bufs)==0);
+
+       assert(meta(FILE_10,bufs)==1);
+       assert(bufs[FIELD_TITLE][0]=='\0');
+       assert(bufs[FIELD_LINK][0]=='\0');
+       assert(bufs[FIELD_PUBDATE][0]=='\0');
+       assert(bufs[FIELD_DESCRIPTION][0]=='\0');
+       assert(bufs[FIELD_GUID][0]=='\0');
+       assert(strcmp(bufs[FIELD_LANGUAGE],"en-uk")==0);
+       strcpy(bufs[FIELD_TITLE],FILE_10);
+       assert(meta(NULL,bufs)==0);
+
+       assert(meta(DIRECTORY_1,bufs)==1);
+       assert(bufs[FIELD_TITLE][0]=='\0');
+       assert(bufs[FIELD_LINK][0]=='\0');
+       assert(bufs[FIELD_PUBDATE][0]=='\0');
+       assert(bufs[FIELD_DESCRIPTION][0]=='\0');
+       assert(bufs[FIELD_GUID][0]=='\0');
+       assert(bufs[FIELD_LANGUAGE][0]=='\0');
+
+       assert(meta(DIRECTORY_2,bufs)==1);
+       assert(bufs[FIELD_TITLE][0]=='\0');
+       assert(bufs[FIELD_LINK][0]=='\0');
+       assert(bufs[FIELD_PUBDATE][0]=='\0');
+       assert(bufs[FIELD_DESCRIPTION][0]=='\0');
+       assert(bufs[FIELD_GUID][0]=='\0');
+       assert(bufs[FIELD_LANGUAGE][0]=='\0');
+
+       assert(meta(DIRECTORY_3,bufs)==1);
+       assert(bufs[FIELD_TITLE][0]=='\0');
+       assert(bufs[FIELD_LINK][0]=='\0');
+       assert(bufs[FIELD_PUBDATE][0]=='\0');
+       assert(bufs[FIELD_DESCRIPTION][0]=='\0');
+       assert(bufs[FIELD_GUID][0]=='\0');
+       assert(bufs[FIELD_LANGUAGE][0]=='\0');
+
+       assert(meta(DIRECTORY_4,bufs)==1);
+       assert(bufs[FIELD_TITLE][0]=='\0');
+       assert(bufs[FIELD_LINK][0]=='\0');
+       assert(bufs[FIELD_PUBDATE][0]=='\0');
+       assert(bufs[FIELD_DESCRIPTION][0]=='\0');
+       assert(bufs[FIELD_GUID][0]=='\0');
+       assert(bufs[FIELD_LANGUAGE][0]=='\0');
+
+       strcpy(bufs[FIELD_TITLE],DIRECTORY_1);
+       assert(meta(NULL,bufs)==1);
+       assert(strcmp(bufs[FIELD_TITLE],DIRECTORY_1)==0);
+       assert(bufs[FIELD_LINK][0]=='\0');
+       assert(bufs[FIELD_PUBDATE][0]=='\0');
+       assert(strcmp(bufs[FIELD_DESCRIPTION],"this is a test description")==0);
+       assert(bufs[FIELD_GUID][0]=='\0');
+       assert(bufs[FIELD_LANGUAGE][0]=='\0');
+
+       strcpy(bufs[FIELD_TITLE],DIRECTORY_2);
+       assert(meta(NULL,bufs)==1);
+       assert(strcmp(bufs[FIELD_TITLE],DIRECTORY_2)==0);
+       assert(bufs[FIELD_LINK][0]=='\0');
+       assert(bufs[FIELD_PUBDATE][0]=='\0');
+       assert(bufs[FIELD_DESCRIPTION][0]=='\0');
+       assert(bufs[FIELD_GUID][0]=='\0');
+       assert(strcmp(bufs[FIELD_LANGUAGE],"en-uk")==0);
+
+       strcpy(bufs[FIELD_TITLE],DIRECTORY_3);
+       assert(meta(NULL,bufs)==1);
+       assert(strcmp(bufs[FIELD_TITLE],DIRECTORY_3)==0);
+       assert(bufs[FIELD_LINK][0]=='\0');
+       assert(strcmp(bufs[FIELD_PUBDATE],"Sun, 16 May 2021 02:55:07 +0000")==0);
+       assert(strcmp(bufs[FIELD_DESCRIPTION],"this a&lt;Mactual&gt;ly show\na new line")==0);
+       assert(bufs[FIELD_GUID][0]=='\0');
+       assert(bufs[FIELD_LANGUAGE][0]=='\0');
+
+       strcpy(bufs[FIELD_TITLE],DIRECTORY_4);
+       assert(meta(NULL,bufs)==0);
+}
+
+void meta_feedurl_test() {
+       char bufs[FIELD_COUNT][BUF_SIZE];
+
+       strcpy(bufs[FIELD_TITLE],FILE_2);
+       assert(meta(NULL,bufs)==1);
+       assert(strcmp(bufs[FIELD_LINK],"0bc6c0de1ff1d26d84187a4d42fb9a2d0bc73a1d.torrent")==0);
+
+       assert(meta(FILE_5,bufs)==1);
+       assert(strcmp(bufs[FIELD_LINK],"0bc6c0de1ff1d26d84187a4d42fb9a2d0bc73a1d.torrent")==0);
+
+       // opt_set_feedurl() should make sure that feedurl has '/' at end       
+       char testurl[] = "https://example.com/feeds/";
+       global_opts.feedurl = testurl;
+
+       strcpy(bufs[FIELD_TITLE],FILE_2);
+       assert(meta(NULL,bufs)==1);
+       assert(strcmp(bufs[FIELD_LINK],"https://example.com/feeds/0bc6c0de1ff1d26d84187a4d42fb9a2d0bc73a1d.torrent")==0);
+
+       assert(meta(FILE_5,bufs)==1);
+       assert(strcmp(bufs[FIELD_LINK],"https://example.com/feeds/0bc6c0de1ff1d26d84187a4d42fb9a2d0bc73a1d.torrent")==0);
+
+       global_opts.feedurl = NULL;
+}
+
+void meta_filename_basic_test() {
+       char tmp[BUF_SIZE];
+       char tmp2[BUF_SIZE];
+
+       strcpy(tmp,"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+
+       assert(meta_filename(tmp,tmp2,10)==-1);
+       assert(meta_filename(tmp,tmp2,BUF_SIZE)==1);
+       assert(strcmp(tmp2,".aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.meta")==0);
+       
+       strcpy(tmp,"/this/is/a/test/path");
+       assert(meta_filename(tmp,tmp2,10)==-1);
+       assert(meta_filename(tmp,tmp2,26)==-1);
+       assert(meta_filename(tmp,tmp2,27)==1);
+       assert(strcmp(tmp2,"/this/is/a/test/.path.meta")==0);
+}
+
+void meta_duplicate_field_test() {
+       char bufs[FIELD_COUNT][BUF_SIZE];
+
+       assert(meta(FILE_11,bufs)==1);
+       assert(strcmp(bufs[FIELD_TITLE],"test2")==0);
+       assert(strcmp(bufs[FIELD_LINK],"link2")==0);
+       assert(strcmp(bufs[FIELD_PUBDATE],"date2")==0);
+       assert(strcmp(bufs[FIELD_DESCRIPTION],"desc2")==0);
+       assert(strcmp(bufs[FIELD_GUID],"guid2")==0);
+       assert(strcmp(bufs[FIELD_LANGUAGE],"lan2")==0);
+}
diff --git a/rss/test/meta.tests.h b/rss/test/meta.tests.h
new file mode 100644 (file)
index 0000000..e15cbe9
--- /dev/null
@@ -0,0 +1,14 @@
+#ifndef __META_TESTS_H_
+#define __META_TESTS_H_
+
+#include "test_utils.h"
+
+#include<meta.h>
+
+int main();
+void meta_basic_tests();
+void meta_feedurl_test();
+void meta_filename_basic_test();
+void meta_duplicate_field_test();
+
+#endif
diff --git a/rss/test/next.tests.c b/rss/test/next.tests.c
new file mode 100644 (file)
index 0000000..39c1540
--- /dev/null
@@ -0,0 +1,32 @@
+#include "next.tests.h"
+
+int main() {
+       setup_env();
+
+       next_basic_tests();
+
+       clean_env();
+
+       return EXIT_SUCCESS;
+}
+
+static int i = 0;
+
+void next_basic_tests() {
+       char buf[1024];
+
+       assert(next(NULL,0)==-1);
+       assert(next(buf,10)==-1);
+       i = 0;
+       assert(next(buf,1024)==1);
+       assert(strcmp(buf,"aaaaaaaaaaaaaaaaaaaa")==0);
+}
+
+int getchar_dummy() {
+       if(i<20) {
+               i++;
+               return 'a';
+       } else {
+               return '\n';
+       }
+}
diff --git a/rss/test/next.tests.h b/rss/test/next.tests.h
new file mode 100644 (file)
index 0000000..2d3a430
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef __NEXT_TESTS_H_
+#define __NEXT_TESTS_H_
+
+#include "test_utils.h"
+
+#include<next.h>
+
+int main();
+void next_basic_tests();
+int getchar_dummy();
+
+#define getchar() getchar_dummy()
+#include "../src/next.c"
+
+#endif
diff --git a/rss/test/opt.desc.tests.c b/rss/test/opt.desc.tests.c
new file mode 100644 (file)
index 0000000..65c49e5
--- /dev/null
@@ -0,0 +1,19 @@
+#include "opt.desc.tests.h"
+
+int main() {
+       setup_env();
+
+       opt_desc_basic_tests();
+
+       clean_env();
+
+       return EXIT_SUCCESS;
+}
+
+void opt_desc_basic_tests() {
+       assert(opt_set_description(NULL)==-1);
+       assert(opt_set_description("")==-1);
+
+       assert(opt_set_description("test")==1);
+       assert(strcmp(global_opts.channel.description,"test")==0);
+}
diff --git a/rss/test/opt.desc.tests.h b/rss/test/opt.desc.tests.h
new file mode 100644 (file)
index 0000000..f5642d8
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef __OPT_DESC_TESTS_H_
+#define __OPT_DESC_TESTS_H_
+
+#include "test_utils.h"
+
+#include<opt.h>
+
+int main();
+void opt_desc_basic_tests();
+
+#endif
diff --git a/rss/test/opt.feed.tests.c b/rss/test/opt.feed.tests.c
new file mode 100644 (file)
index 0000000..ad675a6
--- /dev/null
@@ -0,0 +1,25 @@
+#include "opt.feed.tests.h"
+
+int main() {
+       setup_env();
+
+       opt_feed_basic_tests();
+
+       clean_env();
+
+       return EXIT_SUCCESS;
+}
+
+void opt_feed_basic_tests() {
+       assert(opt_set_feedurl(NULL)==-1);
+       assert(opt_set_feedurl("")==-1);
+
+       assert(opt_set_feedurl("https://test.com")==1);
+       assert(strcmp(global_opts.feedurl,"https://test.com/")==0);
+
+       free(global_opts.feedurl);
+       global_opts.feedurl = NULL;
+
+       assert(opt_set_feedurl("http://test.com/")==1);
+       assert(strcmp(global_opts.feedurl,"http://test.com/")==0);
+}
diff --git a/rss/test/opt.feed.tests.h b/rss/test/opt.feed.tests.h
new file mode 100644 (file)
index 0000000..a0ff551
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef __OPT_FEED_TESTS_H_
+#define __OPT_FEED_TESTS_H_
+
+#include "test_utils.h"
+
+#include<opt.h>
+
+int main();
+void opt_feed_basic_tests();
+
+#endif
diff --git a/rss/test/opt.link.tests.c b/rss/test/opt.link.tests.c
new file mode 100644 (file)
index 0000000..7542289
--- /dev/null
@@ -0,0 +1,19 @@
+#include "opt.link.tests.h"
+
+int main() {
+       setup_env();
+
+       opt_link_basic_tests();
+
+       clean_env();
+
+       return EXIT_SUCCESS;
+}
+
+void opt_link_basic_tests() {
+       assert(opt_set_link(NULL)==-1);
+       assert(opt_set_link("")==-1);
+
+       assert(opt_set_link("alksdjflasdjf")==1);
+       assert(strcmp(global_opts.channel.link,"alksdjflasdjf")==0);
+}
diff --git a/rss/test/opt.link.tests.h b/rss/test/opt.link.tests.h
new file mode 100644 (file)
index 0000000..b45c32b
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef __OPT_LINK_TESTS_H_
+#define __OPT_LINK_TESTS_H_
+
+#include "test_utils.h"
+
+#include<opt.h>
+
+int main();
+void opt_link_basic_tests();
+
+#endif
diff --git a/rss/test/opt.meta.tests.c b/rss/test/opt.meta.tests.c
new file mode 100644 (file)
index 0000000..6ca9118
--- /dev/null
@@ -0,0 +1,21 @@
+#include "opt.meta.tests.h"
+
+int main() {
+       setup_env();
+
+       opt_set_and_parse_meta_basic_tests();
+
+       clean_env();
+
+       return EXIT_SUCCESS;
+}
+
+void opt_set_and_parse_meta_basic_tests() {
+       assert(opt_set_and_parse_meta(NULL)==-1);
+       assert(opt_set_and_parse_meta("notarealfile")==-1);
+
+       assert(opt_set_and_parse_meta(FILE_9)==1);
+       assert(NULL==global_opts.channel.title);
+       assert(strcmp(global_opts.channel.description,"this is a test description")==0);
+       assert(NULL==global_opts.channel.language);
+}
diff --git a/rss/test/opt.meta.tests.h b/rss/test/opt.meta.tests.h
new file mode 100644 (file)
index 0000000..f7ae8e7
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef __OPT_META_TESTS_H_
+#define __OPT_META_TESTS_H_
+
+#include "test_utils.h"
+
+#include<opt.h>
+
+int main();
+void opt_set_and_parse_meta_basic_tests();
+
+#endif
diff --git a/rss/test/opt.name.tests.c b/rss/test/opt.name.tests.c
new file mode 100644 (file)
index 0000000..f40d0d0
--- /dev/null
@@ -0,0 +1,19 @@
+#include "opt.name.tests.h"
+
+int main() {
+       setup_env();
+
+       opt_set_name_basic_tests();
+
+       clean_env();
+
+       return EXIT_SUCCESS;
+}
+
+void opt_set_name_basic_tests() {
+       assert(opt_set_name(NULL)==-1);
+       assert(opt_set_name("")==-1);
+
+       assert(opt_set_name("feedname")==1);
+       assert(strcmp(global_opts.channel.title,"feedname")==0);
+}
diff --git a/rss/test/opt.name.tests.h b/rss/test/opt.name.tests.h
new file mode 100644 (file)
index 0000000..9d0fc2f
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef __OPT_NAME_TESTS_H_
+#define __OPT_NAME_TESTS_H_
+
+#include "test_utils.h"
+
+#include<opt.h>
+
+int main();
+void opt_set_name_basic_tests();
+
+#endif
diff --git a/rss/test/opt.out.tests.c b/rss/test/opt.out.tests.c
new file mode 100644 (file)
index 0000000..3389841
--- /dev/null
@@ -0,0 +1,16 @@
+#include "opt.out.tests.h"
+
+int main() {
+       setup_env();
+
+       opt_out_basic_tests();
+
+       clean_env();
+
+       return EXIT_SUCCESS;
+}
+
+void opt_out_basic_tests() {
+       assert(opt_set_output(NULL)==-1);
+       assert(opt_set_output("/tmp/tempoutfile")==1);
+}
diff --git a/rss/test/opt.out.tests.h b/rss/test/opt.out.tests.h
new file mode 100644 (file)
index 0000000..2f3cf65
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef __OPT_OUT_TESTS_H_
+#define __OPT_OUT_TESTS_H_
+
+#include "test_utils.h"
+
+#include<opt.h>
+
+int main();
+void opt_out_basic_tests();
+
+#endif
diff --git a/rss/test/test_utils.c b/rss/test/test_utils.c
new file mode 100644 (file)
index 0000000..1b2e705
--- /dev/null
@@ -0,0 +1,46 @@
+#include "test_utils.h"
+
+void clean_env() {
+       system("rm -rf " DATADIR);
+}
+
+void reset_env() {
+       clean_env();
+       setup_env();
+}
+
+void setup_env() {
+       clean_env();
+
+       create_test_directory(DATADIR);
+       create_test_directory(DIRECTORY_1);
+       create_test_directory(DIRECTORY_2);
+       create_test_directory(DIRECTORY_3);
+       create_test_directory(DIRECTORY_4);
+       CREATE(1);
+       CREATE(2);
+       CREATE(3);
+       CREATE(4);
+       CREATE(5);
+       CREATE(6);
+       CREATE(7);
+       CREATE(8);
+       CREATE(9);
+       CREATE(10);
+       CREATE(11);
+}
+
+void create_test_directory(const char *directory) {
+       assert(mkdir(directory,0700)==0);
+}
+
+void create_test_file(const char *filename, const char *contents) {
+       FILE *fp;
+
+       printf("creating %s\n",filename);
+       fp = fopen(filename,"w+");
+       assert(NULL!=fp);
+
+       assert(strlen(contents)==fwrite(contents,sizeof(char),strlen(contents),fp));
+       assert(fclose(fp)==0);
+}
diff --git a/rss/test/test_utils.h b/rss/test/test_utils.h
new file mode 100644 (file)
index 0000000..5e3aff7
--- /dev/null
@@ -0,0 +1,54 @@
+#ifndef __TEST_UTILS_H_
+#define __TEST_UTILS_H_
+
+#include<assert.h>
+#include<stdlib.h>
+#include<stdio.h>
+#include<string.h>
+#include<sys/stat.h>
+#include<sys/types.h>
+
+#define DATADIR "/tmp/rss-create"
+
+#define DIRECTORY_1 DATADIR "/test"
+#define DIRECTORY_2 DATADIR "/test2"
+#define DIRECTORY_3 DATADIR "/test/test3"
+#define DIRECTORY_4 DATADIR "/test/.dotfiles"
+
+#define FILE_1 DATADIR "/test/test.txt"
+#define FILE_1_CONTENTS "test"
+#define FILE_2 DATADIR "/test/test2.txt"
+#define FILE_2_CONTENTS "test2"
+#define FILE_3 DATADIR "/test/test3/hello.txt"
+#define FILE_3_CONTENTS "test3"
+#define FILE_4 DATADIR "/test/.test3.meta"
+#define FILE_4_CONTENTS "description=nope\ndescription=this a<Mactual>ly show\\na new line\npubDate=Sun, 16 May 2021 02:55:07 +0000\n"
+#define FILE_5 DATADIR "/test/.test2.txt.meta"
+#define FILE_5_CONTENTS "pubDate=Sat, 15 May 2021 11:01:04 +0000\nlink=0bc6c0de1ff1d26d84187a4d42fb9a2d0bc73a1d.torrent"
+#define FILE_6 DATADIR "/test/.dotfiles/haha.txt"
+#define FILE_6_CONTENTS "hahaha"
+
+#define FILE_7 DATADIR "/test2/.nope.txt.meta"
+#define FILE_7_CONTENTS "guid=hello"
+#define FILE_8 DATADIR "/test2/nope.txt"
+#define FILE_8_CONTENTS "hahahasldkfjasldkfjaslkdfj"
+
+#define FILE_9 DATADIR "/.test.meta"
+#define FILE_9_CONTENTS "description=this is a test description"
+#define FILE_10 DATADIR "/.test2.meta"
+#define FILE_10_CONTENTS "language=en-uk" 
+
+#define FILE_11 DATADIR "/test/test4.txt"
+#define FILE_11_CONTENTS "title=test\ntitle=test2\nlink=link1\nlink=link2\npubDate=date1\npubDate=date2\ndescription=desc1\ndescription=desc2\nguid=guid1\nguid=guid2\nlanguage=lan1\nlanguage=lan2"
+
+#define CREATE_FILE_NAME_LOADER(x) FILE_ ## x
+#define CREATE_FILE_CONTENTS_LOADER(x) FILE_ ## x ## _CONTENTS
+#define CREATE(x) create_test_file(CREATE_FILE_NAME_LOADER(x),CREATE_FILE_CONTENTS_LOADER(x))
+
+void clean_env();
+void reset_env();
+void setup_env();
+void create_test_directory(const char*);
+void create_test_file(const char*,const char*);
+
+#endif
diff --git a/rss/test/util.now.tests.c b/rss/test/util.now.tests.c
new file mode 100644 (file)
index 0000000..0ff4c9f
--- /dev/null
@@ -0,0 +1,20 @@
+#include "util.now.tests.h"
+
+int main() {
+       setup_env();
+
+       util_now_basic_tests();
+
+       clean_env();
+
+       return EXIT_SUCCESS;
+}
+
+void util_now_basic_tests() {
+       char *p;
+       assert(now_string(NULL)==-1);
+
+       assert(now_string(&p)==1);
+
+       free(p);
+}
diff --git a/rss/test/util.now.tests.h b/rss/test/util.now.tests.h
new file mode 100644 (file)
index 0000000..d9453b4
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef __UTIL_NOW_TESTS_H_
+#define __UTIL_NOW_TESTS_H_
+
+#include "test_utils.h"
+
+#include<util.h>
+
+int main();
+void util_now_basic_tests();
+
+#endif
diff --git a/seederd/Makefile.am b/seederd/Makefile.am
new file mode 100644 (file)
index 0000000..b5e4835
--- /dev/null
@@ -0,0 +1,21 @@
+AM_CXXFLAGS = \
+       -DPREFIX=\"$(localstatedir)/seeder\" \
+       -I$(builddir)/inc/ \
+       -I$(srcdir)/inc/
+
+bin_PROGRAMS = seederd
+
+seederd_SOURCES = \
+       src/feed.cpp \
+       src/main.cpp \
+       src/seed.cpp \
+       src/torrent.cpp
+
+seederd_SOURCES += \
+       inc/feed.hpp \
+       inc/main.h \
+       inc/seed.hpp \
+       inc/torrent.hpp
+
+seederd_LDFLAGS = -ltorrent-rasterbar -lboost_system -lpthread
+seederd_LDADD = -lstdc++fs
diff --git a/seederd/inc/consts.h b/seederd/inc/consts.h
new file mode 100644 (file)
index 0000000..757788b
--- /dev/null
@@ -0,0 +1,7 @@
+#ifndef __CONSTS_H_
+#define __CONSTS_H_
+
+#define DATADIR PREFIX "/data"
+#define TORRENTDIR PREFIX "/torrents"
+
+#endif
diff --git a/seederd/inc/feed.hpp b/seederd/inc/feed.hpp
new file mode 100644 (file)
index 0000000..34cb8fa
--- /dev/null
@@ -0,0 +1,22 @@
+#ifndef __FEED_HPP_
+#define __FEED_HPP_
+
+#include<torrent.hpp>
+
+class feed {
+       public:
+               feed(lt::session& session, fs::path feed_path) : m_path(feed_path) {
+                       for(auto &p: fs::directory_iterator(feed_path)) {
+                               if(torrent::file_filter(p.path().string())) {
+                                       torrent t(session,p);
+                               }
+                       }
+                       
+                       torrent t(session,feed_path);
+               }
+               void save();
+       private:
+               fs::path m_path;
+};
+
+#endif
diff --git a/seederd/inc/main.h b/seederd/inc/main.h
new file mode 100644 (file)
index 0000000..dcdcb49
--- /dev/null
@@ -0,0 +1,23 @@
+#ifndef __MAIN_H_
+#define __MAIN_H_
+
+#include<cstdlib>
+#include<filesystem>
+#include<iostream>
+#include<signal.h>
+
+#include<libtorrent/alert_types.hpp>
+#include<libtorrent/session.hpp>
+
+#include<consts.h>
+#include<feed.hpp>
+#include<torrent.hpp>
+
+int main(int,char**);
+void init(lt::session&);
+void handle_alerts(lt::session&);
+void remove_missing(lt::session&);
+int setup_signal_handler();
+void signal_handler(int);
+
+#endif
diff --git a/seederd/inc/seed.hpp b/seederd/inc/seed.hpp
new file mode 100644 (file)
index 0000000..85b99dc
--- /dev/null
@@ -0,0 +1,25 @@
+#ifndef __SEED_HPP_
+#define __SEED_HPP_
+
+#include<filesystem>
+
+#include<boost/make_shared.hpp>
+#include<libtorrent/session.hpp>
+#include<libtorrent/torrent_info.hpp>
+
+namespace fs = std::filesystem;
+
+class seed {
+       public:
+               seed(lt::session& session, std::string torrent_filepath, std::string data_path) {
+                       lt::add_torrent_params params;
+                       params.save_path = data_path;
+                       params.ti = boost::make_shared<lt::torrent_info>(torrent_filepath,0);
+                       params.flags = lt::add_torrent_params::flag_seed_mode;
+
+                       session.async_add_torrent(params);
+               }
+               void static remove();
+};
+
+#endif
diff --git a/seederd/inc/torrent.hpp b/seederd/inc/torrent.hpp
new file mode 100644 (file)
index 0000000..2b61b79
--- /dev/null
@@ -0,0 +1,37 @@
+#ifndef __TORRENT_HPP_
+#define __TORRENT_HPP_
+
+#include<filesystem>
+#include<fstream>
+#include<iostream>
+#include<string>
+
+#include<libtorrent/create_torrent.hpp>
+#include<libtorrent/magnet_uri.hpp>
+#include<libtorrent/torrent_info.hpp>
+
+#include<consts.h>
+#include<seed.hpp>
+
+namespace fs = std::filesystem;
+
+class torrent {
+       public:
+               torrent(lt::session &session, fs::path p) : m_path(p) {
+                       lt::add_files(m_fs,p,file_filter);
+                       if(m_fs.num_files()>0) {
+                               save(session);
+                       } else {
+                               std::cerr << "WARNING: no files found in " << p << "; skipping"<< std::endl;
+                       }
+               };
+               void add(fs::path);
+               void add_link_to_meta(std::string,std::string);
+               bool static file_filter(std::string const&);
+               void save(lt::session&);
+       private:
+               fs::path m_path;
+               lt::file_storage m_fs;
+};
+
+#endif
diff --git a/seederd/src/feed.cpp b/seederd/src/feed.cpp
new file mode 100644 (file)
index 0000000..562ae36
--- /dev/null
@@ -0,0 +1,7 @@
+#include<feed.hpp>
+
+namespace fs = std::filesystem;
+
+void feed::save() {
+       throw std::runtime_error("not implemented\n");
+}
diff --git a/seederd/src/main.cpp b/seederd/src/main.cpp
new file mode 100644 (file)
index 0000000..99103e1
--- /dev/null
@@ -0,0 +1,124 @@
+#include<main.h>
+
+namespace fs = std::filesystem;
+
+int setup_signal_handler() {
+       struct sigaction action;
+
+       action.sa_handler = &signal_handler;
+       action.sa_flags = 0;
+       if(sigemptyset(&action.sa_mask)==-1) {
+               perror("sigemptyset");
+               return -1;
+       }
+
+       if(sigaddset(&action.sa_mask,SIGHUP)==-1) {
+               perror("sigaddset");
+               return -1;
+       }
+
+       if(sigaction(SIGHUP, &action, NULL)==-1) {
+               perror("sigaction");
+               return -1;
+       }
+
+       return 1;
+}
+
+int main(int argc, char **argv) try {
+       if(setup_signal_handler()<0) { return EXIT_FAILURE; }
+
+       std::cout << "creating required directories" << std::endl;
+       fs::create_directories(DATADIR);
+       std::cout << "created " << DATADIR << std::endl;
+       fs::create_directories(TORRENTDIR);
+       std::cout << "created " << TORRENTDIR << std::endl;
+
+       lt::settings_pack settings;
+       lt::high_performance_seed(settings);
+       settings.set_int(lt::settings_pack::active_seeds,-1);
+       settings.set_int(lt::settings_pack::active_limit,-1);
+//     settings.set_int(lt::settings_pack::alert_mask,(lt::alert::session_log_notification + lt::alert::torrent_log_notification));
+       lt::session session(settings);
+
+       init(session);
+
+       handle_alerts(session);
+
+       return EXIT_SUCCESS;
+}
+catch(std::exception const& e) {
+       std::cerr << "ERROR: " << e.what() << std::endl;
+       return EXIT_FAILURE;
+}
+
+void init(lt::session& session) {
+       for(auto &p: fs::directory_iterator(DATADIR)) {
+               if(p.is_directory()) {
+                       if(!torrent::file_filter(p.path().string())) { continue; }
+
+                       std::cout << "creating torrents in " << p.path() << "..." << std::endl;
+               
+                       feed fd(session,p.path());
+
+                       std::cout<< "done!" << std::endl;
+               }
+       }
+
+       remove_missing(session);
+
+       system("update-feeds");
+}
+
+void remove_missing(lt::session& session) {
+       // add all .torrent files
+       std::unordered_map<std::string,fs::path> found;
+       for(auto &p: fs::directory_iterator(TORRENTDIR)) {
+               if(p.is_regular_file()) {
+                       fs::path path = p.path();
+                       found.insert({path.stem().string(),path});
+               }
+       }
+
+       std::vector<lt::torrent_handle> current = session.get_torrents();
+       for(auto t: current) {
+               std::stringstream hash;
+               hash << t.info_hash();
+
+               // remove torrent if .torrent file no longer exists
+               auto search = found.find(hash.str());
+               if(search==found.end()) {
+                       session.remove_torrent(t);
+               }
+       }
+}
+
+
+bool reload = false;
+
+void handle_alerts(lt::session& session) {
+       for (;;) {
+               if(reload) {
+                       reload = false;
+                       std::cout << "reloading configuration..." << std::endl;
+                       init(session);
+                       std::cout << "configuration reloaded" << std::endl;
+               }
+
+               lt::alert const* a = session.wait_for_alert(lt::seconds(10));
+               if(nullptr==a) { continue; }
+
+               std::vector<lt::alert*> alerts;
+               session.pop_alerts(&alerts);
+
+               for (lt::alert const* a : alerts) {
+                       std::cout << "[" << a->type() << "]: " << a->message() << std::endl;
+                       if (lt::alert_cast<lt::torrent_error_alert>(a)) { break; }
+               }
+       }
+       std::cout << "error, shutting down..." << std::endl;
+}
+
+void signal_handler(int signo) {
+       reload = true;
+}
diff --git a/seederd/src/seed.cpp b/seederd/src/seed.cpp
new file mode 100644 (file)
index 0000000..f644630
--- /dev/null
@@ -0,0 +1,5 @@
+#include<seed.hpp>
+
+void seed::remove() {
+       std::cerr << "not implemented\n";
+}
diff --git a/seederd/src/torrent.cpp b/seederd/src/torrent.cpp
new file mode 100644 (file)
index 0000000..e106315
--- /dev/null
@@ -0,0 +1,92 @@
+#include<torrent.hpp>
+
+void torrent::add(fs::path p) {
+       lt::add_files(m_fs,p,file_filter);
+}
+
+void torrent::add_link_to_meta(std::string link, std::string magnet_uri) {
+       std::fstream meta_file;
+
+       fs::path meta_filepath = m_path.parent_path();
+       meta_filepath /= "." + m_path.filename().string() + ".meta";
+
+       bool hasLink = false;
+       bool hasGuid = false;
+
+       meta_file.open(meta_filepath.string(), std::ios_base::in);
+       if(meta_file.is_open()) {
+               std::string line, searchLink, searchGuid;
+               searchLink = "link=" + link;
+               searchGuid = "guid=" + magnet_uri;
+               while(getline(meta_file,line)) {
+                       if(line==searchLink) { hasLink = true; }
+                       if(line==searchGuid) { hasGuid = true; }
+               }
+
+               meta_file.close();
+       }
+
+       meta_file.open(meta_filepath.string(), std::ios_base::out | std::ios_base::app);
+       if(!meta_file.is_open()) {
+               std::cerr << "something went wrong" << std::endl;
+       }
+
+       if(!hasLink) {
+               meta_file << "link=" << link << "\n";
+       }
+
+       if(!hasGuid) {
+               meta_file << "guid=" << magnet_uri << "\n";
+       }
+
+       meta_file.close();
+}
+
+bool torrent::file_filter(std::string const& f) {
+       if(f.empty()) { return false; }
+
+       char const *first = f.c_str();
+       char const *sep = strrchr(first, '/');
+
+       if(nullptr==sep) {
+               sep = first;
+       } else {
+               sep++;
+       }
+
+       if(sep[0]=='.') {
+               std::cerr << "skipped: " << f << std::endl;
+               return false;
+       }
+       
+       std::cerr << "added: " << f << std::endl;
+       return true;
+}
+
+void torrent::save(lt::session &session) {
+       fs::path temp_path = PREFIX;
+       temp_path /= ".tmp.torrent";
+       std::cerr << "temp path: " << temp_path << std::endl;
+
+       fs::path parent_path = m_path.parent_path();
+       lt::create_torrent t(m_fs);
+       lt::set_piece_hashes(t, parent_path);
+
+       std::ofstream out(temp_path, std::ios_base::binary);
+       lt::bencode(std::ostream_iterator<char>(out), t.generate());
+       out.close();
+
+       lt::torrent_info info(temp_path);
+
+       std::stringstream filename;
+       filename << info.info_hash() << ".torrent";
+       fs::path out_path(TORRENTDIR);
+       out_path /= filename.str();
+
+       add_link_to_meta(filename.str(),lt::make_magnet_uri(info));
+
+       if(!fs::exists(out_path)) { rename(temp_path,out_path); }
+       remove(temp_path);
+
+       seed sd(session,out_path.string(),parent_path.string());
+}