]> git.seodisparate.com - c_simple_http/commitdiff
Impl. html cache (mostly done)
authorStephen Seo <seo.disparate@gmail.com>
Wed, 25 Sep 2024 07:12:25 +0000 (16:12 +0900)
committerStephen Seo <seo.disparate@gmail.com>
Wed, 25 Sep 2024 07:12:25 +0000 (16:12 +0900)
TODO: Invalidate cache if it is too old.

src/arg_parse.c
src/helpers.c
src/helpers.h
src/html_cache.c
src/html_cache.h
src/http.c
src/http.h
src/main.c
src/test.c

index 3a0c090cb8e2c413169c497833e235d858a4ea3f..7cb761191d90fc5d0a31efe97116b70dcf1ca4a2 100644 (file)
@@ -108,6 +108,7 @@ Args parse_args(int32_t argc, char **argv) {
       } else {
         printf("Directory \"%s\" exists.\n", args.cache_dir);
       }
+      closedir(d);
     } else {
       fprintf(stderr, "ERROR: Invalid args!\n");
       print_usage();
index b391e22a5b0fd82cb57993ee21919cd24dc77164..c8b8ceedb080adac4a8208f1cfa11821cda42478 100644 (file)
 #include <string.h>
 #include <stdio.h>
 
+// libc includes.
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <errno.h>
+#include <libgen.h>
+
 int c_simple_http_internal_get_string_part_full_size(void *data, void *ud) {
   C_SIMPLE_HTTP_String_Part *part = data;
   size_t *count = ud;
@@ -216,4 +223,42 @@ char *c_simple_http_helper_unescape_uri(const char *uri) {
   return c_simple_http_combine_string_parts(parts);
 }
 
+int c_simple_http_helper_mkdir_tree(const char *path) {
+  // Check if dir already exists.
+  DIR *dir_ptr = opendir(path);
+  if (dir_ptr) {
+    // Directory already exists.
+    closedir(dir_ptr);
+    return 1;
+  } else if (errno == ENOENT) {
+    // Directory doesn't exist, create dir tree.
+    closedir(dir_ptr);
+
+    size_t buf_size = strlen(path) + 1;
+    char *buf = malloc(buf_size);
+    memcpy(buf, path, buf_size - 1);
+    buf[buf_size - 1] = 0;
+
+    char *dirname_buf = dirname(buf);
+    // Recursive call to ensure parent directories are created.
+    int ret = c_simple_http_helper_mkdir_tree(dirname_buf);
+    free(buf);
+    if (ret == 1 || ret == 0) {
+      // Parent directory should be created by now.
+      ret = mkdir(path, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
+      if (ret != 0) {
+        return 4;
+      }
+    } else {
+      return 3;
+    }
+
+    return 0;
+  } else {
+    // Other directory error.
+    closedir(dir_ptr);
+    return 2;
+  }
+}
+
 // vim: et ts=2 sts=2 sw=2
index bd649d9151424a2152766e77070c1fbb348299c6..5e1bd9f2280830055cc8e015d8f31cb6d529fbf0 100644 (file)
@@ -58,6 +58,11 @@ char c_simple_http_helper_hex_to_value(const char upper, const char lower);
 /// non-NULL, it must be free'd.
 char *c_simple_http_helper_unescape_uri(const char *uri);
 
+/// Returns zero if successful. "dirpath" will point to a directory on success.
+/// Returns 1 if the directory already exists.
+/// Other return values are errors.
+int c_simple_http_helper_mkdir_tree(const char *dirpath);
+
 #endif
 
 // vim: et ts=2 sts=2 sw=2
index 0f114b57afa1b121322a76546f187cc48b4ae975..212ccbac081dd7e4ccb1893eb68755df08021727 100644 (file)
 
 // Standard library includes.
 #include <stdint.h>
+#include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
 
+// POSIX includes.
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <errno.h>
+
 // Third-party includes.
 #include <SimpleArchiver/src/data_structures/linked_list.h>
 #include <SimpleArchiver/src/helpers.h>
 // Local includes.
 #include "http.h"
 #include "helpers.h"
+#include "http_template.h"
+
+int c_simple_http_internal_write_filenames_to_cache_file(void *data, void *ud) {
+  char *filename = data;
+  FILE *cache_fd = ud;
+
+  const size_t filename_size = strlen(filename);
+  if (fwrite(filename, 1, filename_size, cache_fd) != filename_size) {
+    return 1;
+  } else if (fwrite("\n", 1, 1, cache_fd) != 1) {
+    return 1;
+  }
+
+  return 0;
+}
 
 char *c_simple_http_path_to_cache_filename(const char *path) {
   __attribute__((cleanup(simple_archiver_helper_cleanup_c_string)))
@@ -185,8 +206,353 @@ int c_simple_http_cache_path(
     const char *path,
     const char *config_filename,
     const char *cache_dir,
+    const C_SIMPLE_HTTP_HTTPTemplates *templates,
     char **buf_out) {
-  // TODO
+  if (!path) {
+    fprintf(stderr, "ERROR cache_path function: path is NULL!\n");
+    return -9;
+  } else if (!config_filename) {
+    fprintf(stderr, "ERROR cache_path function: config_filename is NULL!\n");
+    return -10;
+  } else if (!cache_dir) {
+    fprintf(stderr, "ERROR cache_path function: cache_dir is NULL!\n");
+    return -11;
+  } else if (!templates) {
+    fprintf(stderr, "ERROR cache_path function: templates is NULL!\n");
+    return -12;
+  } else if (!buf_out) {
+    fprintf(stderr, "ERROR cache_path function: buf_out is NULL!\n");
+    return -13;
+  }
+
+  int ret = c_simple_http_helper_mkdir_tree(cache_dir);
+  if (ret != 0 && ret != 1) {
+    fprintf(
+      stderr, "ERROR failed to ensure cache_dir \"%s\" exists!\n", cache_dir);
+    return -15;
+  }
+
+  // Get the cache filename from the path.
+  __attribute__((cleanup(simple_archiver_helper_cleanup_c_string)))
+  char *cache_filename = c_simple_http_path_to_cache_filename(path);
+  if (!cache_filename) {
+    fprintf(stderr, "ERROR Failed to convert path to cache_filename!");
+    return -1;
+  }
+
+  // Combine the cache_dir with cache filename.
+  __attribute__((cleanup(simple_archiver_list_free)))
+  SDArchiverLinkedList *parts = simple_archiver_list_init();
+
+  c_simple_http_add_string_part(parts, cache_dir, 0);
+  c_simple_http_add_string_part(parts, "/", 0);
+  c_simple_http_add_string_part(parts, cache_filename, 0);
+
+  __attribute__((cleanup(simple_archiver_helper_cleanup_c_string)))
+  char *cache_filename_full = c_simple_http_combine_string_parts(parts);
+
+  simple_archiver_list_free(&parts);
+  parts = simple_archiver_list_init();
+
+  if (!cache_filename_full) {
+    fprintf(stderr, "ERROR Failed to create full-path to cache filename!\n");
+    return -2;
+  }
+
+  // Get "stat" info on the cache filename.
+  uint_fast8_t force_cache_update = 0;
+  struct stat cache_file_stat;
+  ret = stat(cache_filename_full, &cache_file_stat);
+  if (ret == -1) {
+    if (errno == ENOENT) {
+      fprintf(stderr, "NOTICE cache file doesn't exist, will create...\n");
+    } else {
+      fprintf(
+        stderr,
+        "ERROR getting stat info on file \"%s\" (errno %d)! "
+        "Assuming out of date!\n",
+        cache_filename_full,
+        errno);
+    }
+    force_cache_update = 1;
+  }
+
+  // Get "stat" info on config file.
+  struct stat config_file_stat;
+  ret = stat(config_filename, &config_file_stat);
+  if (ret == -1) {
+    if (errno == ENOENT) {
+      fprintf(
+        stderr, "ERROR config file \"%s\" doesn't exist!\n", config_filename);
+    } else {
+      fprintf(
+        stderr,
+        "ERROR getting stat info on config file \"%s\" (errno %d)!\n",
+        config_filename,
+        errno);
+    }
+    return -3;
+  }
+
+  if (!force_cache_update) {
+    do {
+      // Check filenames in cache file.
+      __attribute__((cleanup(simple_archiver_helper_cleanup_FILE)))
+      FILE *cache_fd = fopen(cache_filename_full, "r");
+      const size_t buf_size = 1024;
+      __attribute__((cleanup(simple_archiver_helper_cleanup_c_string)))
+      char *buf = malloc(buf_size);
+
+      // Check header.
+      if (fread(buf, 1, 20, cache_fd) != 20) {
+        fprintf(stderr, "ERROR Failed to read header from cache file!\n");
+        return -14;
+      } else if (strncmp(buf, "--- CACHE ENTRY ---\n", 20) != 0) {
+        fprintf(
+          stderr,
+          "WARNING Cache is invalid (bad header), assuming out of date!\n");
+        force_cache_update = 1;
+        break;
+      }
+
+      // Check filenames.
+      size_t buf_idx = 0;
+      while(1) {
+        ret = fgetc(cache_fd);
+        if (ret == EOF) {
+          fprintf(
+            stderr, "WARNING Cache is invalid (EOF), assuming out of date!\n");
+          force_cache_update = 1;
+          break;
+        } else if (ret == '\n') {
+          // Got filename in "buf" of size "buf_idx".
+          if (strncmp(buf, "--- BEGIN HTML ---", 18) == 0) {
+            // Got end header instead of filename.
+            break;
+          } else if (buf_idx < buf_size) {
+            buf[buf_idx++] = 0;
+          } else {
+            fprintf(
+              stderr,
+              "WARNING Cache is invalid (too large filename), assuming out of "
+              "date!\n");
+            force_cache_update = 1;
+            break;
+          }
+
+          struct stat file_stat;
+          ret = stat(buf, &file_stat);
+          if (ret == -1) {
+            if (errno == ENOENT) {
+              fprintf(
+                stderr,
+                "WARNING Invalid filename cache entry \"%s\" (doesn't exist)! "
+                "Assuming out of date!\n",
+                buf);
+              force_cache_update = 1;
+              break;
+            } else {
+              fprintf(
+                stderr,
+                "WARNING Invalid filename cache entry \"%s\" (stat errno %d)! "
+                "Assuming out of date!\n",
+                buf,
+                errno);
+              force_cache_update = 1;
+              break;
+            }
+          }
+
+          if (cache_file_stat.st_mtim.tv_sec < file_stat.st_mtim.tv_sec
+              || (cache_file_stat.st_mtim.tv_sec == file_stat.st_mtim.tv_sec
+                 && cache_file_stat.st_mtim.tv_nsec
+                    < file_stat.st_mtim.tv_nsec)) {
+            // File is newer than cache.
+            force_cache_update = 1;
+            break;
+          }
+
+          buf_idx = 0;
+        } else if (buf_idx < buf_size) {
+          buf[buf_idx++] = (char)ret;
+        } else {
+          fprintf(
+            stderr,
+            "WARNING Cache is invalid (too large filename), assuming out of "
+            "date!\n");
+          force_cache_update = 1;
+          break;
+        }
+      }
+    } while(0);
+  }
+
+  // Compare modification times.
+CACHE_FILE_WRITE_CHECK:
+  if (force_cache_update
+      || cache_file_stat.st_mtim.tv_sec < config_file_stat.st_mtim.tv_sec
+      || (cache_file_stat.st_mtim.tv_sec == config_file_stat.st_mtim.tv_sec
+         && cache_file_stat.st_mtim.tv_nsec < config_file_stat.st_mtim.tv_nsec))
+  {
+    // Cache file is out of date.
+    __attribute__((cleanup(simple_archiver_helper_cleanup_FILE)))
+    FILE *cache_fd = fopen(cache_filename_full, "w");
+    if (fwrite("--- CACHE ENTRY ---\n", 1, 20, cache_fd) != 20) {
+      fprintf(
+        stderr,
+        "ERROR Failed to write to cache file \"%s\"!\n",
+        cache_filename_full);
+      return -5;
+    }
+
+    __attribute__((cleanup(simple_archiver_list_free)))
+    SDArchiverLinkedList *used_filenames = NULL;
+
+    size_t generated_html_size = 0;
+
+    __attribute__((cleanup(simple_archiver_helper_cleanup_c_string)))
+    char *generated_html = c_simple_http_path_to_generated(
+        path, templates, &generated_html_size, &used_filenames);
+
+    if (!generated_html) {
+      fprintf(stderr, "ERROR Failed to generate html for path \"%s\"!\n", path);
+      simple_archiver_helper_cleanup_FILE(&cache_fd);
+      remove(cache_filename_full);
+      return -4;
+    }
+
+    if (simple_archiver_list_get(
+        used_filenames,
+        c_simple_http_internal_write_filenames_to_cache_file,
+        cache_fd)) {
+      fprintf(stderr, "ERROR Failed to write filenames to cache file!\n");
+      return -6;
+    } else if (fwrite("--- BEGIN HTML ---\n", 1, 19, cache_fd) != 19) {
+      fprintf(stderr, "ERROR Failed to write end of cache file header!\n");
+      return -7;
+    } else if (
+        fwrite(
+          generated_html,
+          1,
+          generated_html_size,
+          cache_fd)
+        != generated_html_size) {
+      fprintf(stderr, "ERROR Failed to write html to cache file!\n");
+      return -8;
+    }
+
+    *buf_out = generated_html;
+    generated_html = NULL;
+    return 1;
+  }
+
+  // Cache file is newer.
+  __attribute__((cleanup(simple_archiver_helper_cleanup_FILE)))
+  FILE *cache_fd = fopen(cache_filename_full, "rb");
+
+  const size_t buf_size = 128;
+  __attribute__((cleanup(simple_archiver_helper_cleanup_c_string)))
+  char *buf = malloc(buf_size);
+
+  // Get first header.
+  if (fread(buf, 1, 20, cache_fd) != 20) {
+    fprintf(
+      stderr,
+      "WARNING Invalid cache file (read header), assuming out of date!\n");
+    force_cache_update = 1;
+    goto CACHE_FILE_WRITE_CHECK;
+  } else if (strncmp("--- CACHE ENTRY ---\n", buf, 20) != 0) {
+    fprintf(
+      stderr,
+      "WARNING Invalid cache file (check header), assuming out of date!\n");
+    force_cache_update = 1;
+    goto CACHE_FILE_WRITE_CHECK;
+  }
+
+  // Get filenames end header.
+  uint_fast8_t reached_end_header = 0;
+  size_t buf_idx = 0;
+  while (1) {
+    ret = fgetc(cache_fd);
+    if (ret == EOF) {
+      fprintf(
+        stderr, "WARNING Invalid cache file (EOF), assuming out of date!\n");
+      force_cache_update = 1;
+      goto CACHE_FILE_WRITE_CHECK;
+    } else if (ret == '\n') {
+      if (strncmp("--- BEGIN HTML ---", buf, 18) == 0) {
+        reached_end_header = 1;
+        break;
+      }
+      buf_idx = 0;
+      continue;
+    }
+
+    if (buf_idx < buf_size) {
+      buf[buf_idx++] = (char)ret;
+    }
+  }
+
+  if (!reached_end_header) {
+    fprintf(
+      stderr,
+      "WARNING Invalid cache file (no end header), assuming out of date!\n");
+    force_cache_update = 1;
+    goto CACHE_FILE_WRITE_CHECK;
+  }
+
+  // Remaining bytes in cache_fd is cached html. Fetch it and return it.
+  const long html_start_idx = ftell(cache_fd);
+  if (html_start_idx <= 0) {
+    fprintf(
+      stderr,
+      "WARNING Failed to get position in cache file, assuming "
+      "invalid/out-of-date!\n");
+    force_cache_update = 1;
+    goto CACHE_FILE_WRITE_CHECK;
+  }
+
+  ret = fseek(cache_fd, 0, SEEK_END);
+  if (ret != 0) {
+    fprintf(
+      stderr,
+      "WARNING Failed to seek in cache file, assuming invalid/out-of-date!\n");
+    force_cache_update = 1;
+    goto CACHE_FILE_WRITE_CHECK;
+  }
+  const long html_end_idx = ftell(cache_fd);
+  if (html_end_idx <= 0) {
+    fprintf(
+      stderr,
+      "WARNING Failed to get end position in cache file, assuming "
+      "invalid/out-of-date!\n");
+    force_cache_update = 1;
+    goto CACHE_FILE_WRITE_CHECK;
+  }
+
+  ret = fseek(cache_fd, html_start_idx, SEEK_SET);
+  if (ret != 0) {
+    fprintf(
+      stderr,
+      "WARNING Failed to seek in cache file, assuming invalid/out-of-date!\n");
+    force_cache_update = 1;
+    goto CACHE_FILE_WRITE_CHECK;
+  }
+
+  const size_t html_size = (size_t)html_end_idx - (size_t)html_start_idx + 1;
+  *buf_out = malloc(html_size);
+
+  if (fread(*buf_out, 1, html_size - 1, cache_fd) != html_size - 1) {
+    fprintf(
+      stderr,
+      "WARNING Failed to read html in cache file, assuming "
+      "invalid/out-of-date!\n");
+    force_cache_update = 1;
+    goto CACHE_FILE_WRITE_CHECK;
+  }
+
+  (*buf_out)[html_size - 1] = 0;
+
   return 0;
 }
 
index a382c34e02ca40706050251ef6b58262413b76d8..95a36ab84983184bc1c46fabcf671cb76f3d17ef 100644 (file)
 #ifndef SEODISPARATE_COM_C_SIMPLE_HTTP_HTML_CACHE_H_
 #define SEODISPARATE_COM_C_SIMPLE_HTTP_HTML_CACHE_H_
 
+// Local includes.
+#include "http.h"
+
 /// Must be free'd if non-NULL.
 char *c_simple_http_path_to_cache_filename(const char *path);
 
 /// Must be free'd if non-NULL.
 char *c_simple_http_cache_filename_to_path(const char *cache_filename);
 
-/// Given a "path", returns non-zero if the cache is invalidated.
+/// Given a "path", returns positive-non-zero if the cache is invalidated.
 /// "config_filename" is required to check its timestamp. "cache_dir" is
 /// required to actually get the cache file to check against. "buf_out" will be
 /// populated if non-NULL, and will either be fetched from the cache or from the
 /// config (using http_template). Note that "buf_out" will point to a c-string.
+/// Returns a negative value on error.
 int c_simple_http_cache_path(
   const char *path,
   const char *config_filename,
   const char *cache_dir,
+  const C_SIMPLE_HTTP_HTTPTemplates *templates,
   char **buf_out);
 
 #endif
index 0db0099a63fd4c2a37c4058c5f5a369fe9f26031..e32e54bc28e8de37f476bbd7e00a29bd21ee9814 100644 (file)
@@ -28,6 +28,7 @@
 // Local includes
 #include "http_template.h"
 #include "helpers.h"
+#include "html_cache.h"
 
 #define REQUEST_TYPE_BUFFER_SIZE 16
 #define REQUEST_PATH_BUFFER_SIZE 256
@@ -63,7 +64,9 @@ char *c_simple_http_request_response(
     uint32_t size,
     const C_SIMPLE_HTTP_HTTPTemplates *templates,
     size_t *out_size,
-    enum C_SIMPLE_HTTP_ResponseCode *out_response_code) {
+    enum C_SIMPLE_HTTP_ResponseCode *out_response_code,
+    const char *cache_dir,
+    const char *config_filename) {
   if (out_size) {
     *out_size = 0;
   }
@@ -171,11 +174,42 @@ char *c_simple_http_request_response(
   __attribute__((cleanup(simple_archiver_helper_cleanup_c_string)))
   char *stripped_path = c_simple_http_strip_path(
     request_path_unescaped, strlen(request_path_unescaped));
-  char *generated_buf = c_simple_http_path_to_generated(
-    stripped_path ? stripped_path : request_path_unescaped,
-    templates,
-    &generated_size,
-    NULL); // TODO Use the output parameter "filenames list" here.
+
+  char *generated_buf = NULL;
+
+  if (cache_dir) {
+    int ret = c_simple_http_cache_path(
+      stripped_path ? stripped_path : request_path_unescaped,
+      config_filename,
+      cache_dir,
+      templates,
+      &generated_buf);
+    if (ret < 0) {
+      fprintf(stderr, "ERROR Failed to generate template with cache!\n");
+      if (out_response_code) {
+        if (
+            simple_archiver_hash_map_get(
+              templates->hash_map,
+              stripped_path ? stripped_path : request_path_unescaped,
+              stripped_path
+                ? strlen(stripped_path) + 1
+                : strlen(request_path_unescaped) + 1)
+            == NULL) {
+          *out_response_code = C_SIMPLE_HTTP_Response_404_Not_Found;
+        } else {
+          *out_response_code = C_SIMPLE_HTTP_Response_500_Internal_Server_Error;
+        }
+      }
+      return NULL;
+    }
+    generated_size = strlen(generated_buf);
+  } else {
+    generated_buf = c_simple_http_path_to_generated(
+      stripped_path ? stripped_path : request_path_unescaped,
+      templates,
+      &generated_size,
+      NULL);
+  }
 
   if (!generated_buf || generated_size == 0) {
     fprintf(stderr, "ERROR Unable to generate response html for path \"%s\"!\n",
index d050b27bd5768bc16d982693e60b19a2a33575df..725fbf84cb424bdd4537f5544397290be947a71f 100644 (file)
@@ -48,7 +48,9 @@ char *c_simple_http_request_response(
   uint32_t size,
   const C_SIMPLE_HTTP_HTTPTemplates *templates,
   size_t *out_size,
-  enum C_SIMPLE_HTTP_ResponseCode *out_response_code
+  enum C_SIMPLE_HTTP_ResponseCode *out_response_code,
+  const char *cache_dir,
+  const char *config_filename
 );
 
 /// Takes a PATH string and returns a "bare" path.
index 7c65510a3852c958d9ca1a9a26d02ac95a5594ae..9c9f51c298cda06433e717917c4e3612230a2db5 100644 (file)
@@ -359,7 +359,9 @@ int main(int argc, char **argv) {
         (uint32_t)read_ret,
         &parsed_config,
         &response_size,
-        &response_code);
+        &response_code,
+        args.cache_dir,
+        args.config_file);
       if (response && response_code == C_SIMPLE_HTTP_Response_200_OK) {
         CHECK_ERROR_WRITE(write(connection_fd, "HTTP/1.1 200 OK\n", 16));
         CHECK_ERROR_WRITE(write(connection_fd, "Allow: GET\n", 11));
index e9a5da052e19143475bc876995669b2a20bfb462..9c17dbe06bbc7a43d9535faf7e6c304f36c8eec1 100644 (file)
@@ -4,6 +4,11 @@
 #include <stdlib.h>
 #include <stdint.h>
 
+// POSIX includes.
+#include <unistd.h>
+#include <sys/types.h>
+#include <dirent.h>
+
 // Local includes.
 #include "config.h"
 #include "helpers.h"
@@ -593,6 +598,19 @@ int main(void) {
     CHECK_TRUE(strcmp(buf, "ABC%ZZ") == 0);
     free(buf);
     buf = NULL;
+
+    DIR *dirp = opendir("/tmp/create_dirs_dir");
+    uint_fast8_t dir_exists = dirp ? 1 : 0;
+    closedir(dirp);
+    ASSERT_FALSE(dir_exists);
+
+    int ret = c_simple_http_helper_mkdir_tree("/tmp/create_dirs_dir/dir/");
+    int ret2 = rmdir("/tmp/create_dirs_dir/dir");
+    int ret3 = rmdir("/tmp/create_dirs_dir");
+
+    CHECK_TRUE(ret == 0);
+    CHECK_TRUE(ret2 == 0);
+    CHECK_TRUE(ret3 == 0);
   }
 
   // Test html_cache.
@@ -655,11 +673,11 @@ int main(void) {
 
     ret = c_simple_http_cache_filename_to_path("0x2Fouter0x2Finner");
     ASSERT_TRUE(ret);
-    printf("%s\n", ret);
     CHECK_TRUE(strcmp(ret, "/outer/inner") == 0);
     free(ret);
 
-    ret = c_simple_http_cache_filename_to_path("0x2Fouter0x2Finner0x2F%2F0x2Fmore_inner");
+    ret = c_simple_http_cache_filename_to_path(
+      "0x2Fouter0x2Finner0x2F%2F0x2Fmore_inner");
     ASSERT_TRUE(ret);
     CHECK_TRUE(strcmp(ret, "/outer/inner/%2F/more_inner") == 0);
     free(ret);
@@ -669,7 +687,8 @@ int main(void) {
     CHECK_TRUE(strcmp(ret, "/outer/inner") == 0);
     free(ret);
 
-    ret = c_simple_http_cache_filename_to_path("%2Fouter%2Finner%2F0x2F%2Fmore_inner");
+    ret = c_simple_http_cache_filename_to_path(
+      "%2Fouter%2Finner%2F0x2F%2Fmore_inner");
     ASSERT_TRUE(ret);
     CHECK_TRUE(strcmp(ret, "/outer/inner/0x2F/more_inner") == 0);
     free(ret);
@@ -721,6 +740,172 @@ int main(void) {
     ASSERT_TRUE(ret2);
     CHECK_TRUE(strcmp(ret2, uri3) == 0);
     free(ret2);
+
+    // Set up test config to get template map to test cache.
+    __attribute__((cleanup(test_internal_cleanup_delete_temporary_file)))
+    const char *test_http_template_filename5 =
+      "/tmp/c_simple_http_template_test5.config";
+    __attribute__((cleanup(test_internal_cleanup_delete_temporary_file)))
+    const char *test_http_template_html_filename3 =
+      "/tmp/c_simple_http_template_test3.html";
+    __attribute__((cleanup(test_internal_cleanup_delete_temporary_file)))
+    const char *test_http_template_html_var_filename2 =
+      "/tmp/c_simple_http_template_test_var2.html";
+
+    FILE *test_file = fopen(test_http_template_filename5, "w");
+    ASSERT_TRUE(test_file);
+
+    ASSERT_TRUE(
+      fwrite(
+        "PATH=/\nHTML_FILE=/tmp/c_simple_http_template_test3.html\n",
+        1,
+        56,
+        test_file)
+      == 56);
+    ASSERT_TRUE(
+      fwrite(
+        "VAR_FILE=/tmp/c_simple_http_template_test_var2.html\n",
+        1,
+        52,
+        test_file)
+      == 52);
+    fclose(test_file);
+
+    test_file = fopen(test_http_template_html_filename3, "w");
+    ASSERT_TRUE(test_file);
+
+    ASSERT_TRUE(
+      fwrite(
+        "<body>{{{VAR_FILE}}}</body>\n",
+        1,
+        28,
+        test_file)
+      == 28);
+    fclose(test_file);
+
+    test_file = fopen(test_http_template_html_var_filename2, "w");
+    ASSERT_TRUE(test_file);
+
+    ASSERT_TRUE(
+      fwrite(
+        "Some test text.<br>Yep.",
+        1,
+        23,
+        test_file)
+      == 23);
+    fclose(test_file);
+
+    __attribute__((cleanup(c_simple_http_clean_up_parsed_config)))
+    C_SIMPLE_HTTP_ParsedConfig templates =
+      c_simple_http_parse_config(test_http_template_filename5, "PATH", NULL);
+    ASSERT_TRUE(templates.paths);
+
+    // Run cache function. Should return >0 due to new/first cache entry.
+    __attribute__((cleanup(simple_archiver_helper_cleanup_c_string)))
+    char *buf = NULL;
+    int int_ret = c_simple_http_cache_path(
+      "/",
+      test_http_template_filename5,
+      "/tmp/c_simple_http_cache_dir",
+      &templates,
+      &buf);
+
+    CHECK_TRUE(int_ret > 0);
+    ASSERT_TRUE(buf);
+    CHECK_TRUE(strcmp(buf, "<body>Some test text.<br>Yep.</body>\n") == 0);
+    free(buf);
+    buf = NULL;
+
+    // Check/get size of cache file.
+    FILE *cache_file = fopen("/tmp/c_simple_http_cache_dir/ROOT", "r");
+    uint_fast8_t cache_file_exists = cache_file ? 1 : 0;
+    fseek(cache_file, 0, SEEK_END);
+    const long cache_file_size_0 = ftell(cache_file);
+    fclose(cache_file);
+    ASSERT_TRUE(cache_file_exists);
+
+    // Re-run cache function, checking that it is not invalidated.
+    int_ret = c_simple_http_cache_path(
+      "/",
+      test_http_template_filename5,
+      "/tmp/c_simple_http_cache_dir",
+      &templates,
+      &buf);
+    CHECK_TRUE(int_ret == 0);
+    ASSERT_TRUE(buf);
+    CHECK_TRUE(strcmp(buf, "<body>Some test text.<br>Yep.</body>\n") == 0);
+    free(buf);
+    buf = NULL;
+
+    // Check/get size of cache file.
+    cache_file = fopen("/tmp/c_simple_http_cache_dir/ROOT", "r");
+    cache_file_exists = cache_file ? 1 : 0;
+    fseek(cache_file, 0, SEEK_END);
+    const long cache_file_size_1 = ftell(cache_file);
+    fclose(cache_file);
+    ASSERT_TRUE(cache_file_exists);
+    CHECK_TRUE(cache_file_size_0 == cache_file_size_1);
+
+    // Change a file used by the template for PATH=/ .
+    test_file = fopen(test_http_template_html_var_filename2, "w");
+    ASSERT_TRUE(test_file);
+
+    ASSERT_TRUE(
+      fwrite(
+        "Alternate test text.<br>Yep.",
+        1,
+        28,
+        test_file)
+      == 28);
+    fclose(test_file);
+
+    // Re-run cache function, checking that it is invalidated.
+    int_ret = c_simple_http_cache_path(
+      "/",
+      test_http_template_filename5,
+      "/tmp/c_simple_http_cache_dir",
+      &templates,
+      &buf);
+    CHECK_TRUE(int_ret > 0);
+    ASSERT_TRUE(buf);
+    CHECK_TRUE(strcmp(buf, "<body>Alternate test text.<br>Yep.</body>\n") == 0);
+    free(buf);
+    buf = NULL;
+
+    // Get/check size of cache file.
+    cache_file = fopen("/tmp/c_simple_http_cache_dir/ROOT", "r");
+    cache_file_exists = cache_file ? 1 : 0;
+    fseek(cache_file, 0, SEEK_END);
+    const long cache_file_size_2 = ftell(cache_file);
+    fclose(cache_file);
+    ASSERT_TRUE(cache_file_exists);
+    CHECK_TRUE(cache_file_size_0 != cache_file_size_2);
+
+    // Re-run cache function, checking that it is not invalidated.
+    int_ret = c_simple_http_cache_path(
+      "/",
+      test_http_template_filename5,
+      "/tmp/c_simple_http_cache_dir",
+      &templates,
+      &buf);
+    CHECK_TRUE(int_ret == 0);
+    ASSERT_TRUE(buf);
+    CHECK_TRUE(strcmp(buf, "<body>Alternate test text.<br>Yep.</body>\n") == 0);
+    free(buf);
+    buf = NULL;
+
+    // Get/check size of cache file.
+    cache_file = fopen("/tmp/c_simple_http_cache_dir/ROOT", "r");
+    cache_file_exists = cache_file ? 1 : 0;
+    fseek(cache_file, 0, SEEK_END);
+    const long cache_file_size_3 = ftell(cache_file);
+    fclose(cache_file);
+    ASSERT_TRUE(cache_file_exists);
+    CHECK_TRUE(cache_file_size_2 == cache_file_size_3);
+
+    // Cleanup.
+    remove("/tmp/c_simple_http_cache_dir/ROOT");
+    rmdir("/tmp/c_simple_http_cache_dir");
   }
 
   RETURN()