From 3da53413ce87cdeeadb67401cf794ae534484a37 Mon Sep 17 00:00:00 2001 From: Matt Mascarenhas Date: Mon, 17 Sep 2018 19:06:31 +0100 Subject: [PATCH] cinera.c: Revved resources Database version 4 Revving resources involves hashing asset files and appending a query string to their URLs. Additionally we monitor asset files for changes and edit their new checksum hash into all HTML files citing them. This commit also introduces new template tags for assets (listed below) with which you may instruct Cinera to rev assets of your choice. There is further information about this in the help (-h) and the README.md Amongst other minor changes, we now support unset $XDG_CACHE_HOME and $HOME - Thanks to insofaras for wordexp() New flags: -Q Set query string New template tags: __CINERA_ASSET__ __CINERA_CSS__ __CINERA_IMAGE__ __CINERA_JS__ Renamed template tag: __CINERA_SEARCH__ (was __CINERA_INDEX__) --- README.md | 130 +- cinera/cinera.c | 5828 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 4175 insertions(+), 1783 deletions(-) diff --git a/README.md b/README.md index 50e5b9b..1e46538 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,11 @@ deployment ## Cinera +### Install the dependencies + +1. flex (for hmmlib) +2. curl (for cinera) + ### Download, and prepare the parser 1. `git clone git@gitssh.handmade.network:Annotation-Pushers/Annotation-System.git` @@ -13,10 +18,6 @@ deployment Note: For each parser update, remember to make and copy it into place. -### Install the dependency - -1. curl - ### Build 1. `$SHELL cinera.c` @@ -57,56 +58,75 @@ By default all directories - input and output - are set to the current working directory. Typical operation will involve setting these flags: -d Project Input Directory, the directory where the .hmml files reside - -r Root Directory, path shallower than or equal to the CSS, Images and JS - directories - -R Root URL, corresponding to the Root Directory (optional if the Output - Base Directory resides in the Root Directory) + -r Asset Root Directory, path shallower than or equal to the CSS, Images and + JS directories + -R Asset Root URL, corresponding to the Root Directory -c CSS Directory, relative to Root -i Images Directory, relative to Root -j JS Directory, relative to Root -b Output Base Directory, location of the table of contents / search page -B Output Base URL, corresponding to the Output Base Directory -t Template Directory - -x Index Template Location, relative to Template Directory + -x Search Template Location, relative to Template Directory -y Player Template Location, relative to Template Directory #### Templates -*Index Template* -- `` _the necessary `.css` and `.js` files_ -- `` _the table of contents, and search functionality_ +*Search Template* +- `` _to put inside your own ``_ +- `` _the table of contents and search functionality_ *Player Template* -- `` _the necessary `.css` and `.js` files, and - charset setting_ -- `` _ _the menu bar that typically appears above the - player in my samples_ -- `` _the player_ -- `` _the filter state objects and `.js` file, which - must come after both the MENUS and PLAYER tags_ +- `` _to put inside your own ``_ +- `` +- `` +- `` _must come after `` and ``_ *Optional tags available for use in your Player Template* -- `` _the day / episode name, intended to be used - inside your own `` element, but may be used wherever and as many times - as you want on your page_ -- `<!-- __CINERA_VIDEO_ID__ -->` _the unique identifer of the video as provided - by the VoD platform_ +- `<!-- __CINERA_TITLE__ -->` +- `<!-- __CINERA_VIDEO_ID__ -->` -*Other available tags* -- `<!-- __CINERA_PROJECT__ -->` _the full name of the project_ -- `<!-- __CINERA_PROJECT_ID__ -->` _the ID of the project_ -- `<!-- __CINERA_THEME__ -->` _the theme of the project_ -- `<!-- __CINERA_URL__ -->` _the URL where we have derived the page will be - publically accessibly, only really usable if BaseURL is set (-B)_ +*Other tags available for use in either template* +- Asset tags: + - `<!-- __CINERA_ASSET__ path.ext -->` + General purpose tag that outputs the URL of the specified asset + relative to the Asset Root URL (-R) + - `<!-- __CINERA_IMAGE__ path.ext -->` + General purpose tag that outputs the URL of the specified asset + relative to the Images Directory (-i) + - `<!-- __CINERA_CSS__ path.ext -->` + Convenience tag that outputs a <link rel="stylesheet"...> node + for the specified asset relative to the CSS Directory (-c), for + use inside your <head> block + - `<!-- __CINERA_JS__ path.ext -->` + Convenience tag that outputs a <script type="text/javascript"...> + node for the specified asset relative to the JS Directory (-j), + for use wherever a <script> node is valid +The path.ext in these tags supports parent directories to locate the +asset file relative to its specified type directory (generic, CSS, image +or JS), including the "../" directory, and paths containing spaces must +be surrounded with double-quotes (\-escapable if the quoted path itself +contains double-quotes). + +All these asset tags additionally perform revving, appending a query +string (-Q) and the file's checksum to the URL. Changes to a file +trigger a rehash and edit of all HTML pages citing this asset. + +- `<!-- __CINERA_PROJECT__ -->` +- `<!-- __CINERA_PROJECT_ID__ -->` +- `<!-- __CINERA_THEME__ -->` +- `<!-- __CINERA_URL__ -->` _Only really usable if BaseURL is set (-B)_ - `<!-- __CINERA_CUSTOM0__ -->` - `<!-- __CINERA_CUSTOM1__ -->` - `<!-- __CINERA_CUSTOM2__ -->` -- ⋮ + ⋮ - `<!-- __CINERA_CUSTOM15__ -->` - _Freeform buffers for small snippets of localised information, e.g. a single - `<a>` element or perhaps a `<!-- comment -->` They correspond to the custom0 - to custom15 attributes in the [video] node in your .hmml files 0 to 11 may - hold up to 255 characters 12 to 15 may hold up to 1023 characters_ + Freeform buffers for small snippets of localised information, e.g. a + single `<a>` element or perhaps a `<!-- comment -->` + They correspond to the custom0 to custom15 attributes in the [video] + node in your .hmml files + 0 to 11 may hold up to 255 characters + 12 to 15 may hold up to 1023 characters Feel free to play with templates to your heart's content. If you do anything invalid, _Cinera_ will tell you what's wrong. @@ -117,10 +137,10 @@ invalid, _Cinera_ will tell you what's wrong. Options: Paths: (advisedly universal, but may be set per-(sub)project as required) - -r <root directory> - Override default root directory (".") - -R <root URL> - Override default root URL ("") + -r <assets root directory> + Override default assets root directory (".") + -R <assets root URL> + Override default assets root URL ("") IMPORTANT: -r and -R must correspond to the same location UNSUPPORTED: If you move files from RootDir, the RootURL should correspond to the resulting location @@ -131,18 +151,22 @@ invalid, _Cinera_ will tell you what's wrong. Override default images directory (""), relative to root -j <JS directory path> Override default JS directory (""), relative to root + -Q <revved resources query string> + Override default query string ("r") + To disable revved resources, set an empty string with -Q "" Project Settings: -p <project ID> - Set the project ID, equal to the "project" field in the HMML files - NOTE: Setting the project ID triggers PROJECT EDITION + Set the project ID, triggering PROJECT EDITION -m <default medium> Override default default medium ("programming") Known project defaults: bitwise: programming book: research - pcalc: programming + coad: research + reader: research riscy: programming + risc: speech chat: speech code: programming intro-to-c: programming @@ -157,9 +181,6 @@ invalid, _Cinera_ will tell you what's wrong. -s <style> Set the style / theme, corresponding to a cinera__*.css file This is equal to the "project" field in the HMML files by default - -q - Quit after syncing with annotation files in project input directory - UNSUPPORTED: This is likely to be removed in the future Project Input Paths -d <annotations directory> @@ -167,8 +188,8 @@ invalid, _Cinera_ will tell you what's wrong. -t <templates directory> Override default templates directory (".") - -x <index template location> - Set index template file path, either absolute or relative to + -x <search template location> + Set search template file path, either absolute or relative to template directory, and enable integration -y <player template location> Set player template file path, either absolute or relative @@ -181,8 +202,8 @@ invalid, _Cinera_ will tell you what's wrong. Override default base URL ("") NOTE: This must be set, if -n or -a are to be used - -n <index location> - Override default index location (""), relative to base + -n <search location> + Override default search location (""), relative to base -a <player location> Override default player location (""), relative to base NOTE: The PlayerURLPrefix is currently hardcoded in cinera.c but @@ -192,14 +213,18 @@ invalid, _Cinera_ will tell you what's wrong. -o <output location> Override default output player location ("out.html") - -e - Display (examine) index file and exit + -1 + Open search result links in the same browser tab + NOTE: Ideal for a guide embedded in an iframe -f Force integration with an incomplete template -g Ignore video privacy status NOTE: For use with projects whose videos are known to all be public, to save us having to check their privacy status + -q + Quit after syncing with annotation files in project input directory + UNSUPPORTED: This is likely to be removed in the future -w Force quote cache rebuild (memory aid: "wget") @@ -207,6 +232,9 @@ invalid, _Cinera_ will tell you what's wrong. Override default log level (0), where n is from 0 (terse) to 7 (verbose) -u <seconds> Override default update interval (4) + + -e + Display (examine) database and exit -v Display version and exit -h diff --git a/cinera/cinera.c b/cinera/cinera.c index 65ff893..ebc7899 100644 --- a/cinera/cinera.c +++ b/cinera/cinera.c @@ -8,20 +8,18 @@ ctime -end ${0%.*}.ctm exit #endif +#include <stdint.h> typedef struct { - unsigned int Major, Minor, Patch; + uint32_t Major, Minor, Patch; } version; version CINERA_APP_VERSION = { .Major = 0, - .Minor = 5, - .Patch = 66 + .Minor = 6, + .Patch = 0 }; -// TODO(matt): Copy in the DB 3 stuff from cinera_working.c -#define CINERA_DB_VERSION 3 - #include <stdarg.h> // NOTE(matt): varargs #include <stdio.h> // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror #include <stdlib.h> // NOTE(matt): calloc, malloc, free @@ -36,12 +34,21 @@ version CINERA_APP_VERSION = { #include <string.h> // NOTE(matt): strerror #include <errno.h> //NOTE(matt): errno #include <sys/inotify.h> // NOTE(matt): inotify +#include <wordexp.h> + +#define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW +#include <fcntl.h> // NOTE(matt): open() +#define __USE_XOPEN2K // NOTE(matt): readlink() #include <unistd.h> // NOTE(matt): sleep() typedef unsigned int bool; #define TRUE 1 #define FALSE 0 +#define enum8(type) int8_t +#define enum16(type) int16_t +#define enum32(type) int32_t + #define DEBUG 0 #define DEBUG_MEM 0 @@ -55,16 +62,28 @@ clock_t TIMING_START; #define MAX_PROJECT_ID_LENGTH 31 #define MAX_PROJECT_NAME_LENGTH 63 +#define MAX_BASE_DIR_LENGTH 127 #define MAX_BASE_URL_LENGTH 127 #define MAX_RELATIVE_PAGE_LOCATION_LENGTH 31 #define MAX_PLAYER_URL_PREFIX_LENGTH 15 -#define MAX_BASE_FILENAME_LENGTH 31 -#define MAX_TITLE_LENGTH 128 - (MAX_BASE_FILENAME_LENGTH + 1) - (int)sizeof(link_insertion_offsets) - (int)sizeof(unsigned short int) - 1 // NOTE(matt): We size this such that index_metadata is 128 bytes total +#define MAX_ROOT_DIR_LENGTH 127 +#define MAX_ROOT_URL_LENGTH 127 +#define MAX_RELATIVE_ASSET_LOCATION_LENGTH 31 +#define MAX_BASE_FILENAME_LENGTH 31 +#define MAX_TITLE_LENGTH 128 - (MAX_BASE_FILENAME_LENGTH + 1) - (int)sizeof(link_insertion_offsets) - (int)sizeof(unsigned short int) - 1 // NOTE(matt): We size this such that db_entry is 128 bytes total + +#define MAX_ASSET_FILENAME_LENGTH 63 + +// TODO(matt): Stop distinguishing between short / long and lift the size limit once we're on the LUT #define MAX_CUSTOM_SNIPPET_SHORT_LENGTH 255 #define MAX_CUSTOM_SNIPPET_LONG_LENGTH 1023 +#define ArrayCount(A) sizeof(A)/sizeof(*(A)) +#define Assert(Expression) if(!(Expression)) { printf("l.%d: \e[1;31mAssertion failure\e[0m\n", __LINE__); __asm__("int3"); } +#define FOURCC(String) ((uint32_t)(String[0] << 0) | (uint32_t)(String[1] << 8) | (uint32_t)(String[2] << 16) | (uint32_t)(String[3] << 24)) + enum { EDITION_SINGLE, @@ -92,7 +111,8 @@ enum MODE_EXAMINE = 1 << 2, MODE_NOCACHE = 1 << 3, MODE_NOPRIVACY = 1 << 4, - MODE_SINGLETAB = 1 << 5 + MODE_SINGLETAB = 1 << 5, + MODE_NOREVVEDRESOURCE = 1 << 6 } modes; enum @@ -104,6 +124,7 @@ enum RC_ERROR_HMML, RC_ERROR_MAX_REFS, RC_ERROR_MEMORY, + RC_ERROR_PARSING, RC_ERROR_PROJECT, RC_ERROR_QUOTE, RC_ERROR_SEEK, @@ -128,11 +149,11 @@ typedef struct typedef struct { // Universal - char CacheDir[256]; - int Edition; - int LogLevel; - int Mode; - int UpdateInterval; + char CacheDir[256]; + enum8(editions) Edition; + enum8(log_levels) LogLevel; + enum8(modes) Mode; + int UpdateInterval; // Advisedly universal, although could be per-project char *RootDir; // Absolute @@ -140,6 +161,7 @@ typedef struct char *CSSDir; // Relative to Root{Dir,URL} char *ImagesDir; // Relative to Root{Dir,URL} char *JSDir; // Relative to Root{Dir,URL} + char *QueryString; // Per Project char *ProjectID; @@ -149,13 +171,13 @@ typedef struct // Per Project - Input char *ProjectDir; // Absolute char *TemplatesDir; // Absolute - char *TemplateIndexLocation; // Relative to TemplatesDir ??? + char *TemplateSearchLocation; // Relative to TemplatesDir ??? char *TemplatePlayerLocation; // Relative to TemplatesDir ??? // Per Project - Output char *BaseDir; // Absolute char *BaseURL; - char *IndexLocation; // Relative to Base{Dir,URL} + char *SearchLocation; // Relative to Base{Dir,URL} char *PlayerLocation; // Relative to Base{Dir,URL} char *PlayerURLPrefix; /* NOTE(matt): This will become a full blown customisable output URL. For now it simply replaces the ProjectID */ @@ -167,13 +189,6 @@ typedef struct char *OutIntegratedLocation; } config; -// NOTE(matt): Globals -config Config = {}; -arena MemoryArena; -time_t LastPrivacyCheck; -time_t LastQuoteFetch; -// - typedef struct { char *Location; @@ -190,11 +205,106 @@ typedef struct int FileSize; } file_buffer; +char *AssetTypeNames[] = +{ + "Generic", + "CSS", + "Image", + "JavaScript" +}; + +enum +{ + ASSET_GENERIC, + ASSET_CSS, + ASSET_IMG, + ASSET_JS, + ASSET_TYPE_COUNT +} asset_types; + +typedef struct asset +{ + int32_t Hash; + enum8(asset_types) Type; + char Filename[MAX_ASSET_FILENAME_LENGTH + 1]; + int32_t FilenameAt:29; + int32_t Known:1; + int32_t OffsetLandmarks:1; + int32_t DeferredUpdate:1; + uint32_t SearchLandmarkCapacity; + uint32_t SearchLandmarkCount; + uint32_t *SearchLandmark; + uint32_t PlayerLandmarkCapacity; + uint32_t PlayerLandmarkCount; + uint32_t *PlayerLandmark; +} asset; + +asset BuiltinAssets[] = +{ + { 0, ASSET_CSS, "cinera.css" }, + { 0, ASSET_CSS }, // NOTE(matt): .Filename set by InitBuiltinAssets() + { 0, ASSET_CSS, "cinera_topics.css" }, + { 0, ASSET_IMG, "cinera_icon_filter.png" }, + { 0, ASSET_JS, "cinera_search.js" }, + { 0, ASSET_JS, "cinera_player_pre.js" }, + { 0, ASSET_JS, "cinera_player_post.js" }, +}; + +enum +{ + ASSET_CSS_CINERA, + ASSET_CSS_THEME, + ASSET_CSS_TOPICS, + ASSET_IMG_FILTER, + ASSET_JS_SEARCH, + ASSET_JS_PLAYER_PRE, + ASSET_JS_PLAYER_POST, + BUILTIN_ASSETS_COUNT, +} builtin_assets; + +typedef struct +{ + int Count; + int Capacity; + asset *Asset; +} assets; + +enum +{ + WT_HMML, + WT_ASSET +} watch_types; + +typedef struct +{ + int Descriptor; + enum8(watch_types) Type; + char Path[MAX_ROOT_DIR_LENGTH + 1 + MAX_RELATIVE_ASSET_LOCATION_LENGTH]; +} watch_handle; + +typedef struct +{ + int Count; + int Capacity; + watch_handle *Handle; +} watch_handles; + +// DBVersion 1 +typedef struct { unsigned int DBVersion; version AppVersion; version HMMLVersion; unsigned int EntryCount; } db_header1; +typedef struct { int Size; char BaseFilename[32]; } db_entry1; +typedef struct { file_buffer File; file_buffer Metadata; db_header1 Header; db_entry1 Entry; } database1; +// + +// DBVersion 2 +typedef struct { unsigned int DBVersion; version AppVersion; version HMMLVersion; unsigned int EntryCount; char SearchLocation[32]; char PlayerLocation[32]; } db_header2; +typedef db_entry1 db_entry2; +typedef struct { file_buffer File; file_buffer Metadata; db_header2 Header; db_entry2 Entry; } database2; +// + // TODO(matt): Increment CINERA_DB_VERSION! typedef struct { - // NOTE(matt): Consider augmenting this to contain such stuff as: "hex signature" - unsigned int CurrentDBVersion; // NOTE(matt): Put this first to aid reliability + unsigned int CurrentDBVersion; version CurrentAppVersion; version CurrentHMMLVersion; @@ -206,16 +316,16 @@ typedef struct char ProjectID[MAX_PROJECT_ID_LENGTH + 1]; char ProjectName[MAX_PROJECT_NAME_LENGTH + 1]; char BaseURL[MAX_BASE_URL_LENGTH + 1]; - char IndexLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1]; + char SearchLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1]; char PlayerLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1]; - char PlayerURLPrefix[MAX_PLAYER_URL_PREFIX_LENGTH + 1]; // TODO(matt): Replace this with the OutputPath, when we add that -} index_header; + char PlayerURLPrefix[MAX_PLAYER_URL_PREFIX_LENGTH + 1]; +} db_header3; typedef struct { unsigned int PrevStart, NextStart; unsigned short int PrevEnd, NextEnd; -} link_insertion_offsets; // NOTE(matt): Relative +} link_insertion_offsets; // NOTE(matt): PrevStart is Absolute (or relative to start of file), the others are Relative to PrevStart typedef struct { @@ -223,22 +333,117 @@ typedef struct unsigned short int Size; char BaseFilename[MAX_BASE_FILENAME_LENGTH + 1]; char Title[MAX_TITLE_LENGTH + 1]; -} index_metadata; +} db_entry3; typedef struct { file_buffer File; file_buffer Metadata; - index_header Header; - index_metadata Entry; -} index; -// TODO(matt): Increment CINERA_DB_VERSION! + db_header3 Header; + db_entry3 Entry; +} database3; + +#pragma pack(push, 1) +typedef struct +{ + uint32_t HexSignature; // 'CNRA' + uint32_t CurrentDBVersion; + version CurrentAppVersion; + version CurrentHMMLVersion; + + uint32_t InitialDBVersion; + version InitialAppVersion; + version InitialHMMLVersion; + + uint32_t BlockCount; +} db_header4; typedef struct { - buffer IncludesIndex; - buffer Search; - buffer Index; // NOTE(matt): This buffer is malloc'd separately, rather than claimed from the memory_arena + uint32_t BlockID; // 'NTRY' + uint16_t Count; + char ProjectID[MAX_PROJECT_ID_LENGTH + 1]; + char ProjectName[MAX_PROJECT_NAME_LENGTH + 1]; + char BaseURL[MAX_BASE_URL_LENGTH + 1]; + char SearchLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1]; + char PlayerLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1]; + char PlayerURLPrefix[MAX_PLAYER_URL_PREFIX_LENGTH + 1]; // TODO(matt): Replace this with the OutputPath, when we add that +} db_header_entries4; + +typedef db_entry3 db_entry4; + +typedef struct +{ + uint32_t BlockID; // 'ASET' + uint16_t Count; + char RootDir[MAX_ROOT_DIR_LENGTH + 1]; + char RootURL[MAX_ROOT_URL_LENGTH + 1]; + char CSSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1]; + char ImagesDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1]; + char JSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1]; +} db_header_assets4; + +typedef struct +{ + int32_t Hash; + uint32_t LandmarkCount; + enum8(asset_types) Type; + char Filename[MAX_ASSET_FILENAME_LENGTH + 1]; +} db_asset4; + +typedef struct +{ + int32_t EntryIndex; + uint32_t Position; +} db_landmark4; + +typedef struct +{ + file_buffer File; + file_buffer Metadata; + + db_header4 Header; + db_header_entries4 EntriesHeader; + db_entry4 Entry; + db_header_assets4 AssetsHeader; + db_asset4 Asset; + db_landmark4 Landmark; +} database4; +#pragma pack(pop) + +#define CINERA_DB_VERSION 4 + +#define db_header db_header4 +#define db_header_entries db_header_entries4 +#define db_entry db_entry4 +#define db_header_assets db_header_assets4 +#define db_asset db_asset4 +#define db_landmark db_landmark4 +#define database database4 +// TODO(matt): Increment CINERA_DB_VERSION! + +// NOTE(matt): Globals +arena MemoryArena; +arena TemplateArena; +config Config; +assets Assets; +int inotifyInstance; +watch_handles WatchHandles; +database DB; +time_t LastPrivacyCheck; +time_t LastQuoteFetch; +// + +enum +{ + PAGE_TYPE_SEARCH = -1, +} page_type_indices; + +typedef struct +{ + buffer IncludesSearch; + buffer SearchEntry; + buffer Search; // NOTE(matt): This buffer is malloc'd separately, rather than claimed from the memory_arena buffer IncludesPlayer; buffer Menus; buffer Player; @@ -265,7 +470,7 @@ typedef struct char ProjectName[MAX_PROJECT_NAME_LENGTH + 1]; char Theme[MAX_PROJECT_NAME_LENGTH + 1]; char Title[MAX_TITLE_LENGTH + 1]; - char URLIndex[MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1]; + char URLSearch[MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1]; char URLPlayer[MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1]; char VideoID[16]; } buffers; @@ -276,7 +481,7 @@ enum TAG_INCLUDES, // Contents Page Mandatory - TAG_INDEX, + TAG_SEARCH, // Player Page Mandatory TAG_MENUS, @@ -306,66 +511,70 @@ enum TAG_VIDEO_ID, // Anywhere Optional + TAG_ASSET, + TAG_CSS, + TAG_IMAGE, + TAG_JS, TAG_PROJECT, TAG_PROJECT_ID, TAG_THEME, TAG_URL, -} template_tags; + TEMPLATE_TAG_COUNT, +} template_tag_codes; -typedef struct -{ - int Code; // template_tags - char *Tag; -} tag; +char *TemplateTags[] = { + "__CINERA_INCLUDES__", -tag Tags[] = { - { TAG_INCLUDES, "__CINERA_INCLUDES__" }, + "__CINERA_SEARCH__", - { TAG_INDEX, "__CINERA_INDEX__" }, + "__CINERA_MENUS__", + "__CINERA_PLAYER__", + "__CINERA_SCRIPT__", - { TAG_MENUS, "__CINERA_MENUS__" }, - { TAG_PLAYER, "__CINERA_PLAYER__" }, - { TAG_SCRIPT, "__CINERA_SCRIPT__" }, + "__CINERA_CUSTOM0__", + "__CINERA_CUSTOM1__", + "__CINERA_CUSTOM2__", + "__CINERA_CUSTOM3__", + "__CINERA_CUSTOM4__", + "__CINERA_CUSTOM5__", + "__CINERA_CUSTOM6__", + "__CINERA_CUSTOM7__", + "__CINERA_CUSTOM8__", + "__CINERA_CUSTOM9__", + "__CINERA_CUSTOM10__", + "__CINERA_CUSTOM11__", - { TAG_CUSTOM0, "__CINERA_CUSTOM0__" }, - { TAG_CUSTOM1, "__CINERA_CUSTOM1__" }, - { TAG_CUSTOM2, "__CINERA_CUSTOM2__" }, - { TAG_CUSTOM3, "__CINERA_CUSTOM3__" }, - { TAG_CUSTOM4, "__CINERA_CUSTOM4__" }, - { TAG_CUSTOM5, "__CINERA_CUSTOM5__" }, - { TAG_CUSTOM6, "__CINERA_CUSTOM6__" }, - { TAG_CUSTOM7, "__CINERA_CUSTOM7__" }, - { TAG_CUSTOM8, "__CINERA_CUSTOM8__" }, - { TAG_CUSTOM9, "__CINERA_CUSTOM9__" }, - { TAG_CUSTOM10, "__CINERA_CUSTOM10__" }, - { TAG_CUSTOM11, "__CINERA_CUSTOM11__" }, + "__CINERA_CUSTOM12__", + "__CINERA_CUSTOM13__", + "__CINERA_CUSTOM14__", + "__CINERA_CUSTOM15__", - { TAG_CUSTOM12, "__CINERA_CUSTOM12__" }, - { TAG_CUSTOM13, "__CINERA_CUSTOM13__" }, - { TAG_CUSTOM14, "__CINERA_CUSTOM14__" }, - { TAG_CUSTOM15, "__CINERA_CUSTOM15__" }, + "__CINERA_TITLE__", + "__CINERA_VIDEO_ID__", - { TAG_TITLE, "__CINERA_TITLE__" }, - { TAG_VIDEO_ID, "__CINERA_VIDEO_ID__" }, - - { TAG_PROJECT, "__CINERA_PROJECT__" }, - { TAG_PROJECT_ID, "__CINERA_PROJECT_ID__" }, - { TAG_THEME, "__CINERA_THEME__" }, - { TAG_URL, "__CINERA_URL__" }, + "__CINERA_ASSET__", + "__CINERA_CSS__", + "__CINERA_IMAGE__", + "__CINERA_JS__", + "__CINERA_PROJECT__", + "__CINERA_PROJECT_ID__", + "__CINERA_THEME__", + "__CINERA_URL__", }; typedef struct { int Offset; - int TagCode; + uint32_t AssetIndex; + enum8(template_tags) TagCode; } tag_offset; typedef struct { - char Filename[256]; - tag_offset Tag[16]; int Validity; // NOTE(matt): Bitmask describing which page the template is valid for, i.e. contents and / or player page int TagCount; + char Filename[256]; + tag_offset Tag[16]; } template_metadata; typedef struct @@ -412,61 +621,74 @@ typedef struct int Count; } categories; +char *SupportIcons[] = +{ + "cinera_sprite_patreon.png", + "cinera_sprite_sendowl.png", +}; + +typedef enum +{ + ICON_PATREON = BUILTIN_ASSETS_COUNT, + ICON_SENDOWL, + SUPPORT_ICON_COUNT, +} support_icons; + // TODO(matt): Parse this stuff out of a config file typedef struct { - char *Username; - char *CreditedName; - char *HomepageURL; - char *SupportIcon; - char *SupportURL; + char *Username; + char *CreditedName; + char *HomepageURL; + enum8(support_icons) SupportIconIndex; + char *SupportURL; } credential_info; credential_info Credentials[] = { - { "a_waterman", "Andrew Waterman", "https://www.linkedin.com/in/andrew-waterman-76805788", "", ""}, - { "y_lee", "Yunsup Lee", "https://www.linkedin.com/in/yunsup-lee-385b692b/", "", ""}, - { "AndrewJDR", "Andrew Johnson", "", "", ""}, - { "AsafGartner", "Asaf Gartner", "", "", ""}, - { "BretHudson", "Bret Hudson", "http://www.brethudson.com/", "cinera_sprite_patreon.png", "https://www.patreon.com/indieFunction"}, - { "ChronalDragon", "Andrew Chronister", "http://chronal.net/", "", ""}, - { "Kelimion", "Jeroen van Rijn", "https://handmade.network/home", "", ""}, - { "Mannilie", "Emmanuel Vaccaro", "http://emmanuelvaccaro.com/", "", ""}, - { "Miblo", "Matt Mascarenhas", "https://miblodelcarpio.co.uk/", "cinera_sprite_sendowl.png", "https://miblodelcarpio.co.uk/cinera#pledge"}, - { "Mr4thDimention", "Allen Webster", "http://www.4coder.net/", "", ""}, - { "Pseudonym73", "Andrew Bromage", "https://twitter.com/deguerre", "", ""}, - { "Quel_Solaar", "Eskil Steenberg", "http://quelsolaar.com/", "", ""}, - { "ZedZull", "Jay Waggle", "", "", ""}, - { "abnercoimbre", "Abner Coimbre", "https://handmade.network/m/abnercoimbre", "", ""}, - { "brianwill", "Brian Will", "http://brianwill.net/blog/", "", ""}, - { "cbloom", "Charles Bloom", "http://cbloomrants.blogspot.co.uk/", "", ""}, - { "cmuratori", "Casey Muratori", "https://handmadehero.org", "cinera_sprite_sendowl.png", "https://handmadehero.org/fund"}, - { "csnover", "Colin Snover", "https://zetafleet.com/", "", ""}, - { "debiatan", "Miguel Lechón", "http://blog.debiatan.net/", "", ""}, - { "dspecht", "Dustin Specht", "", "", ""}, - { "effect0r", "Cory Henderlite", "", "", ""}, - { "ffsjs", "ffsjs", "", "", ""}, - { "fierydrake", "Mike Tunnicliffe", "", "", ""}, - { "garlandobloom", "Matthew VanDevander", "https://lowtideproductions.com/", "cinera_sprite_patreon.png", "https://www.patreon.com/mv"}, - { "ikerms", "Iker Murga", "", "", ""}, - { "insofaras", "Alex Baines", "https://abaines.me.uk/", "", ""}, - { "jacebennett", "Jace Bennett", "", "", ""}, - { "jon", "Jonathan Blow", "http://the-witness.net/news/", "", ""}, - { "jpike", "Jacob Pike", "", "", ""}, - { "martincohen", "Martin Cohen", "http://blog.coh.io/", "", ""}, - { "miotatsu", "Mio Iwakura", "http://riscy.tv/", "cinera_sprite_patreon.png", "https://patreon.com/miotatsu"}, - { "nothings", "Sean Barrett", "https://nothings.org/", "", ""}, - { "pervognsen", "Per Vognsen", "https://github.com/pervognsen/bitwise/", "", ""}, - { "philipbuuck", "Philip Buuck", "http://philipbuuck.com/", "", ""}, - { "powerc9000", "Clay Murray", "http://claymurray.website/", "", ""}, - { "rygorous", "Fabian Giesen", "https://fgiesen.wordpress.com/", "", ""}, - { "schme", "Kasper Sauramo", "", "", ""}, - { "sssmcgrath", "Shawn McGrath", "http://www.dyadgame.com/", "", ""}, - { "thehappiecat", "Anne", "https://www.youtube.com/c/TheHappieCat", "cinera_sprite_patreon.png", "https://www.patreon.com/thehappiecat"}, - { "theinternetftw", "Ben Craddock", "", "", ""}, - { "wheatdog", "Tim Liou", "http://stringbulbs.com/", "", ""}, - { "williamchyr", "William Chyr", "http://williamchyr.com/", "", ""}, - { "wonchun", "Won Chun", "https://twitter.com/won3d", "", ""}, + { "a_waterman", "Andrew Waterman", "https://www.linkedin.com/in/andrew-waterman-76805788" }, + { "y_lee", "Yunsup Lee", "https://www.linkedin.com/in/yunsup-lee-385b692b/" }, + { "AndrewJDR", "Andrew Johnson" }, + { "AsafGartner", "Asaf Gartner" }, + { "BretHudson", "Bret Hudson", "http://www.brethudson.com/", ICON_PATREON, "https://www.patreon.com/indieFunction"}, + { "ChronalDragon", "Andrew Chronister", "http://chronal.net/" }, + { "Kelimion", "Jeroen van Rijn", "https://handmade.network/home" }, + { "Mannilie", "Emmanuel Vaccaro", "http://emmanuelvaccaro.com/" }, + { "Miblo", "Matt Mascarenhas", "https://miblodelcarpio.co.uk/", ICON_SENDOWL, "https://miblodelcarpio.co.uk/cinera#pledge"}, + { "Mr4thDimention", "Allen Webster", "http://www.4coder.net/" }, + { "Pseudonym73", "Andrew Bromage", "https://twitter.com/deguerre" }, + { "Quel_Solaar", "Eskil Steenberg", "http://quelsolaar.com/" }, + { "ZedZull", "Jay Waggle" }, + { "abnercoimbre", "Abner Coimbre", "https://handmade.network/m/abnercoimbre" }, + { "brianwill", "Brian Will", "http://brianwill.net/blog/" }, + { "cbloom", "Charles Bloom", "http://cbloomrants.blogspot.co.uk/" }, + { "cmuratori", "Casey Muratori", "https://handmadehero.org", ICON_SENDOWL, "https://handmadehero.org/fund"}, + { "csnover", "Colin Snover", "https://zetafleet.com/" }, + { "debiatan", "Miguel Lechón", "http://blog.debiatan.net/" }, + { "dspecht", "Dustin Specht" }, + { "effect0r", "Cory Henderlite" }, + { "ffsjs", "ffsjs" }, + { "fierydrake", "Mike Tunnicliffe" }, + { "garlandobloom", "Matthew VanDevander", "https://lowtideproductions.com/", ICON_PATREON, "https://www.patreon.com/mv"}, + { "ikerms", "Iker Murga" }, + { "insofaras", "Alex Baines", "https://abaines.me.uk/" }, + { "jacebennett", "Jace Bennett" }, + { "jon", "Jonathan Blow", "http://the-witness.net/news/" }, + { "jpike", "Jacob Pike" }, + { "martincohen", "Martin Cohen", "http://blog.coh.io/" }, + { "miotatsu", "Mio Iwakura", "http://riscy.tv/", ICON_PATREON, "https://patreon.com/miotatsu"}, + { "nothings", "Sean Barrett", "https://nothings.org/" }, + { "pervognsen", "Per Vognsen", "https://github.com/pervognsen/bitwise/" }, + { "philipbuuck", "Philip Buuck", "http://philipbuuck.com/" }, + { "powerc9000", "Clay Murray", "http://claymurray.website/" }, + { "rygorous", "Fabian Giesen", "https://fgiesen.wordpress.com/" }, + { "schme", "Kasper Sauramo" }, + { "sssmcgrath", "Shawn McGrath", "http://www.dyadgame.com/" }, + { "thehappiecat", "Anne", "https://www.youtube.com/c/TheHappieCat", ICON_PATREON, "https://www.patreon.com/thehappiecat"}, + { "theinternetftw", "Ben Craddock" }, + { "wheatdog", "Tim Liou", "http://stringbulbs.com/" }, + { "williamchyr", "William Chyr", "http://williamchyr.com/" }, + { "wonchun", "Won Chun", "https://twitter.com/won3d" }, }; typedef struct @@ -481,17 +703,17 @@ category_medium CategoryMedium[] = // medium icon written name { "admin", "🗹", "Administrivia"}, { "afk", "…" , "Away from Keyboard"}, - { "authored", "🗪", "Chat Comment"}, // TODO(matt): Conditionally handle Chat vs Guest Comments + { "authored", "🗪", "Chat Comment"}, { "blackboard", "🖌", "Blackboard"}, { "drawing", "🎨", "Drawing"}, { "experience", "🍷", "Experience"}, { "hat", "🎩", "Hat"}, { "multimedia", "🎬", "Media Clip"}, { "owl", "🦉", "Owl of Shame"}, - { "programming", "🖮", "Programming"}, // TODO(matt): Potentially make this configurable per project + { "programming", "🖮", "Programming"}, { "rant", "💢", "Rant"}, { "research", "📖", "Research"}, - { "run", "🏃", "In-Game"}, // TODO(matt): Potentially make this configurable per project + { "run", "🏃", "In-Game"}, // TODO(matt): Potentially make this written name configurable per project { "speech", "🗩", "Speech"}, { "trivia", "🎲", "Trivia"}, }; @@ -505,12 +727,13 @@ enum typedef struct { - char *ProjectID; - char *FullName; - char *Unit; // e.g. Day, Episode, Session - int NumberingScheme; // numbering_schemes - char *Medium; - char *AltURLPrefix; // NOTE(matt): This currently just straight up replaces the ProjectID in the player pages' output directories + char *ProjectID; + char *FullName; + char *Unit; // e.g. Day, Episode, Session + enum8(numbering_schemes) NumberingScheme; // numbering_schemes + char *Medium; + char *AltURLPrefix; // NOTE(matt): This currently just straight up replaces the ProjectID in the player + // pages' output directories } project_info; project_info ProjectInfo[] = @@ -539,7 +762,53 @@ project_info ProjectInfo[] = { "sysadmin", "SysAdmin", "Session", NS_LINEAR, "admin", "" }, }; -#define ArrayCount(A) sizeof(A)/sizeof(*(A)) +char *ColourStrings[] = +{ + "\e[0m", + "\e[0;33m", + "\e[0;34m", + "\e[0;35m", "\e[0;35m", + "\e[1;30m", "\e[1;30m", + "\e[1;31m", + "\e[1;32m", "\e[1;32m", + "\e[1;33m", +}; + +enum +{ + CS_END, + CS_WARNING, + CS_PRIVATE, + CS_ONGOING, CS_PURPLE, + CS_COMMENT, CS_DELETION, + CS_ERROR, + CS_ADDITION, CS_SUCCESS, + CS_REINSERTION, +} colour_strings; + +typedef struct +{ + char *Name; + enum8(colour_strings) Colour; +} edit_type; + +enum +{ + EDIT_INSERTION, + EDIT_APPEND, + EDIT_REINSERTION, + EDIT_DELETION, + EDIT_ADDITION, +} edit_types; + +edit_type EditTypes[] = +{ + { "Inserted", CS_ADDITION }, + { "Appended", CS_ADDITION }, + { "Reinserted", CS_REINSERTION }, + { "Deleted", CS_DELETION }, + { "Added", CS_ADDITION } +}; void Clear(char *String, int Size) @@ -680,6 +949,24 @@ CopyStringToBufferNoFormat_(int LineNumber, buffer *Dest, char *String) *Dest->Ptr = '\0'; } +#define CopyStringToBufferNoFormatL(Dest, Length, String) CopyStringToBufferNoFormatL_(__LINE__, Dest, Length, String) +void +CopyStringToBufferNoFormatL_(int LineNumber, buffer *Dest, int Length, char *String) +{ + char *Start = String; + for(int i = 0; i < Length; ++i) + { + *Dest->Ptr++ = *String++; + } + if(Dest->Ptr - Dest->Location >= Dest->Size) + { + fprintf(stderr, "CopyStringToBufferNoFormat(%s) call on line %d cannot accommodate %d-character string:\n" + "%s\n", Dest->ID, LineNumber, StringLength(Start), Start); + __asm__("int3"); + } + *Dest->Ptr = '\0'; +} + #define CopyStringToBufferHTMLSafe(Dest, String) CopyStringToBufferHTMLSafe_(__LINE__, Dest, String) void CopyStringToBufferHTMLSafe_(int LineNumber, buffer *Dest, char *String) @@ -730,10 +1017,47 @@ CopyStringToBufferHTMLSafeBreakingOnSlash_(int LineNumber, buffer *Dest, char *S } if(Dest->Ptr - Dest->Location >= Dest->Size) { - fprintf(stderr, "CopyStringToBufferHTMLSafeBreakingOnSlash_(%s) call on line %d cannot accommodate %d(+1)-character HTML-sanitised string:\n" + fprintf(stderr, "CopyStringToBufferHTMLSafeBreakingOnSlash(%s) call on line %d cannot accommodate %d(+1)-character HTML-sanitised string:\n" "%s\n", Dest->ID, LineNumber, Length, Start); __asm__("int3"); } + *Dest->Ptr = '\0'; +} + +#define CopyStringToBufferHTMLPercentEncoded(Dest, String) CopyStringToBufferHTMLPercentEncoded_(__LINE__, Dest, String) +void +CopyStringToBufferHTMLPercentEncoded_(int LineNumber, buffer *Dest, char *String) +{ + char *Start = String; + int Length = StringLength(String); + while(*String) + { + switch(*String) + { + case ' ': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '0'; Length += 2; break; + case '\"': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '2'; Length += 2; break; + case '%': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '5'; Length += 2; break; + case '&': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '6'; Length += 2; break; + case '<': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = 'C'; Length += 2; break; + case '>': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = 'E'; Length += 2; break; + case '?': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = 'F'; Length += 2; break; + case '\\': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '5'; *Dest->Ptr++ = 'C'; Length += 2; break; + case '^': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '5'; *Dest->Ptr++ = 'E'; Length += 2; break; + case '`': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '6'; *Dest->Ptr++ = '0'; Length += 2; break; + case '{': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '7'; *Dest->Ptr++ = 'B'; Length += 2; break; + case '|': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '7'; *Dest->Ptr++ = 'C'; Length += 2; break; + case '}': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '7'; *Dest->Ptr++ = 'D'; Length += 2; break; + default: *Dest->Ptr++ = *String; break; + } + ++String; + } + if(Dest->Ptr - Dest->Location >= Dest->Size) + { + fprintf(stderr, "CopyStringToBufferHTMLPercentEncodedL(%s) call on line %d cannot accommodate %d(+1)-character percent-encoded string:\n" + "%s\n", Dest->ID, LineNumber, Length, Start); + __asm__("int3"); + } + *Dest->Ptr = '\0'; } #define CopyBuffer(Dest, Src) CopyBuffer_(__LINE__, Dest, Src) @@ -757,6 +1081,7 @@ CopyBuffer_(int LineNumber, buffer *Dest, buffer *Src) void CopyBufferSized_(int LineNumber, buffer *Dest, buffer *Src, int Size) { + // NOTE(matt): Similar to CopyBuffer(), just without null-terminating Src->Ptr = Src->Location; while(Src->Ptr - Src->Location < Size) { @@ -815,6 +1140,129 @@ StringsDifferT(char *A, // NOTE(matt): Null-terminated string } } +enum +{ + C_SEEK_FORWARDS, + C_SEEK_BACKWARDS +} seek_directions; + +enum +{ + C_SEEK_START, // First character of string + C_SEEK_BEFORE, // Character before first character + C_SEEK_END, // Last character of string + C_SEEK_AFTER // Character after last character +} seek_positions; + +int +SeekBufferForString(buffer *Buffer, char *String, + enum8(seek_directions) Direction, + enum8(seek_positions) Position) +{ + // TODO(matt): Optimise? Some means of analysing the String to increment + // the pointer in bigger strides + + // Perhaps count up runs of consecutive chars and seek for the char with + // the longest run, in strides of that run-length + + char *InitialLocation = Buffer->Ptr; + if(Direction == C_SEEK_FORWARDS) + { + while(Buffer->Ptr - Buffer->Location < Buffer->Size - StringLength(String) + && StringsDifferT(String, Buffer->Ptr, 0)) + { + ++Buffer->Ptr; + } + } + else + { + while(Buffer->Ptr > Buffer->Location + && StringsDifferT(String, Buffer->Ptr, 0)) + { + --Buffer->Ptr; + } + } + + if(StringsDifferT(String, Buffer->Ptr, 0)) + { + Buffer->Ptr = InitialLocation; + return RC_UNFOUND; + } + + switch(Position) + { + case C_SEEK_START: + break; + case C_SEEK_BEFORE: + if(Buffer->Ptr > Buffer->Location) + { + --Buffer->Ptr; + break; + } + else + { + return RC_ERROR_SEEK; // Ptr remains at string start + } + case C_SEEK_END: + Buffer->Ptr += StringLength(String) - 1; + break; + case C_SEEK_AFTER: + if(Buffer->Size >= Buffer->Ptr - Buffer->Location + StringLength(String)) + { + Buffer->Ptr += StringLength(String); + break; + } + else + { + return RC_ERROR_SEEK; // Ptr remains at string start + // NOTE(matt): Should it, however, be left at the end of the string? + } + } + return RC_SUCCESS; +} + +int +StripComponentFromPath(char *Path) +{ + char *Ptr = Path + StringLength(Path) - 1; + if(Ptr < Path) { return RC_ERROR_DIRECTORY; } + while(Ptr > Path && *Ptr != '/') + { + --Ptr; + } + *Ptr = '\0'; + return RC_SUCCESS; +} + +int ClaimBuffer(buffer *Buffer, char *ID, int Size); +void DeclaimBuffer(buffer *Buffer); + +int +ResolvePath(char *Path) +{ + buffer B; + ClaimBuffer(&B, "ResolvedPath", StringLength(Path) + 1); + CopyStringToBufferNoFormat(&B, Path); + + B.Ptr = B.Location; + while(SeekBufferForString(&B, "/../", C_SEEK_FORWARDS, C_SEEK_END) == RC_SUCCESS) + { + char *NextComponentHead = B.Ptr; + int RemainingChars = StringLength(NextComponentHead); + --B.Ptr; + SeekBufferForString(&B, "/", C_SEEK_BACKWARDS, C_SEEK_BEFORE); + SeekBufferForString(&B, "/", C_SEEK_BACKWARDS, C_SEEK_START); + CopyStringToBufferNoFormat(&B, NextComponentHead); + Clear(B.Ptr, B.Size - (B.Ptr - B.Location)); + B.Ptr -= RemainingChars; + } + + CopyStringNoFormat(Path, StringLength(Path) + 1, B.Location); + + DeclaimBuffer(&B); + return RC_SUCCESS; +} + int MakeDir(char *Path) { @@ -827,13 +1275,8 @@ MakeDir(char *Path) { return RC_ERROR_DIRECTORY; } - while(Path[i] != '/' && i > 0) - { - --i; - } + if(StripComponentFromPath(Path) == RC_ERROR_DIRECTORY) { return RC_ERROR_DIRECTORY; } ++Ancestors; - Path[i] = '\0'; - if(i == 0) { return RC_ERROR_DIRECTORY; } } while(Ancestors > 0) { @@ -918,16 +1361,6 @@ FreeBuffer(buffer *Buffer) #endif } -#if 0 -#define ClaimBuffer(MemoryArena, Buffer, ID, Size) if(__ClaimBuffer(MemoryArena, Buffer, ID, Size))\ -{\ - fprintf(stderr, "%s:%d: MemoryArena cannot contain %s of size %d\n", __FILE__, __LINE__, ID, Size);\ - hmml_free(&HMML);\ - FreeBuffer(MemoryArena);\ - return 1;\ -}; -#endif - int ClaimBuffer(buffer *Buffer, char *ID, int Size) { @@ -969,15 +1402,19 @@ DeclaimBuffer(buffer *Buffer) LogUsage(Buffer); if(PercentageUsed >= 95.0f) { - // TODO(matt): Implement either dynamically growing buffers, or phoning home to matt@handmadedev.org + // TODO(matt): Implement either dynamically growing buffers, or phoning home to miblodelcarpio@gmail.com LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed); - fprintf(stderr, "\e[1;31mWarning\e[0m: %s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed); + fprintf(stderr, "%sWarning%s: %s used %.2f%% of its allotted memory\n", + ColourStrings[CS_ERROR], ColourStrings[CS_END], + Buffer->ID, PercentageUsed); } else if(PercentageUsed >= 80.0f) { - // TODO(matt): Implement either dynamically growing buffers, or phoning home to matt@handmadedev.org + // TODO(matt): Implement either dynamically growing buffers, or phoning home to miblodelcarpio@gmail.com LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed); - fprintf(stderr, "\e[0;33mWarning\e[0m: %s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed); + fprintf(stderr, "%sWarning%s: %s used %.2f%% of its allotted memory\n", + ColourStrings[CS_WARNING], ColourStrings[CS_END], + Buffer->ID, PercentageUsed); } Buffer->Size = 0; } @@ -999,7 +1436,7 @@ RewindBuffer(buffer *Buffer) enum { - TEMPLATE_INDEX, + TEMPLATE_SEARCH, TEMPLATE_PLAYER, TEMPLATE_BESPOKE } templates; @@ -1074,11 +1511,11 @@ ConstructTemplatePath(template *Template, int TemplateType) int InitTemplate(template **Template) { - if(MemoryArena.Ptr - MemoryArena.Location + sizeof(template) > MemoryArena.Size) + if(TemplateArena.Ptr - TemplateArena.Location + sizeof(template) > TemplateArena.Size) { return RC_ARENA_FULL; } - *Template = (template *)MemoryArena.Ptr; + *Template = (template *)TemplateArena.Ptr; Clear((*Template)->Metadata.Filename, 256); // NOTE(matt): template_metadata specifies Filename[256] (*Template)->Metadata.Validity = 0; (*Template)->Metadata.TagCount = 0; @@ -1087,7 +1524,7 @@ InitTemplate(template **Template) (*Template)->Metadata.Tag[i].Offset = 0; (*Template)->Metadata.Tag[i].TagCode = 0; } - MemoryArena.Ptr += sizeof(template); + TemplateArena.Ptr += sizeof(template); return RC_SUCCESS; } @@ -1097,10 +1534,7 @@ ClaimTemplate(template **Template, char *Location, int TemplateType) CopyString((*Template)->Metadata.Filename, sizeof((*Template)->Metadata.Filename), "%s", Location); ConstructTemplatePath((*Template), TemplateType); - if(TemplateType == TEMPLATE_BESPOKE) - { - fprintf(stderr, "\e[0;35mPacking\e[0m template: %s\n", (*Template)->Metadata.Filename); - } + fprintf(stderr, "%sPacking%s template: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], (*Template)->Metadata.Filename); FILE *File; if(!(File = fopen((*Template)->Metadata.Filename, "r"))) @@ -1112,13 +1546,13 @@ ClaimTemplate(template **Template, char *Location, int TemplateType) } fseek(File, 0, SEEK_END); (*Template)->Buffer.Size = ftell(File); - if(MemoryArena.Ptr - MemoryArena.Location + (*Template)->Buffer.Size > MemoryArena.Size) + if(TemplateArena.Ptr - TemplateArena.Location + (*Template)->Buffer.Size > TemplateArena.Size) { Clear((*Template)->Metadata.Filename, 256); // NOTE(matt): template_metadata specifies Filename[256] return RC_ARENA_FULL; } - (*Template)->Buffer.Location = MemoryArena.Ptr; + (*Template)->Buffer.Location = TemplateArena.Ptr; (*Template)->Buffer.Ptr = (*Template)->Buffer.Location; (*Template)->Buffer.ID = (*Template)->Metadata.Filename; @@ -1126,11 +1560,11 @@ ClaimTemplate(template **Template, char *Location, int TemplateType) fread((*Template)->Buffer.Location, (*Template)->Buffer.Size, 1, File); fclose(File); - MemoryArena.Ptr += (*Template)->Buffer.Size; + TemplateArena.Ptr += (*Template)->Buffer.Size; #if DEBUG printf(" ClaimTemplate(%s): %d\n" - " Total ClaimedMemory: %ld\n\n", (*Template)->Metadata.Filename, (*Template)->Buffer.Size, MemoryArena.Ptr - MemoryArena.Location); + " Total ClaimedMemory: %ld\n\n", (*Template)->Metadata.Filename, (*Template)->Buffer.Size, TemplateArena.Ptr - TemplateArena.Location); #endif return RC_SUCCESS; } @@ -1146,13 +1580,13 @@ DeclaimTemplate(template *Template) Template->Metadata.Tag[i].TagCode = 0; } Template->Metadata.TagCount = 0; - MemoryArena.Ptr -= (*Template).Buffer.Size; + TemplateArena.Ptr -= (*Template).Buffer.Size; #if DEBUG printf("DeclaimTemplate(%s)\n" " Total ClaimedMemory: %ld\n\n", (*Template).Metadata.Filename, - MemoryArena.Ptr - MemoryArena.Location); + TemplateArena.Ptr - TemplateArena.Location); #endif return RC_SUCCESS; } @@ -1263,26 +1697,20 @@ SanitisePunctuation(char *String) return String; } -enum -{ - INCLUDE_CSS, - INCLUDE_Images, - INCLUDE_JS, -} include_types; - enum { PAGE_PLAYER = 1 << 0, - PAGE_INDEX = 1 << 1 + PAGE_SEARCH = 1 << 1 } pages; void -ConstructURLPrefix(buffer *URLPrefix, int IncludeType, int PageType) +ConstructURLPrefix(buffer *URLPrefix, enum8(asset_types) AssetType, enum8(pages) PageType) { RewindBuffer(URLPrefix); if(StringsDiffer(Config.RootURL, "")) { - CopyStringToBuffer(URLPrefix, "%s/", Config.RootURL); + CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.RootURL); + CopyStringToBuffer(URLPrefix, "/"); } else { @@ -1296,24 +1724,27 @@ ConstructURLPrefix(buffer *URLPrefix, int IncludeType, int PageType) } } - switch(IncludeType) + switch(AssetType) { - case INCLUDE_CSS: + case ASSET_CSS: if(StringsDiffer(Config.CSSDir, "")) { - CopyStringToBuffer(URLPrefix, "%s/", Config.CSSDir); + CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.CSSDir); + CopyStringToBuffer(URLPrefix, "/"); } break; - case INCLUDE_Images: + case ASSET_IMG: if(StringsDiffer(Config.ImagesDir, "")) { - CopyStringToBuffer(URLPrefix, "%s/", Config.ImagesDir); + CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.ImagesDir); + CopyStringToBuffer(URLPrefix, "/"); } break; - case INCLUDE_JS: + case ASSET_JS: if(StringsDiffer(Config.JSDir, "")) { - CopyStringToBuffer(URLPrefix, "%s/", Config.JSDir); + CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.JSDir); + CopyStringToBuffer(URLPrefix, "/"); } break; } @@ -1340,6 +1771,746 @@ enum CreditsError_NoCredentials } credits_errors; +int +ReadFileIntoBuffer(file_buffer *File, int BufferPadding) +{ + if(!(File->Handle = fopen(File->Path, "r"))) // TODO(matt): Fuller error handling + { + return RC_ERROR_FILE; + } + + fseek(File->Handle, 0, SEEK_END); + File->FileSize = ftell(File->Handle); + File->Buffer.Size = File->FileSize + 1 + BufferPadding; // NOTE(matt): +1 to accommodate a NULL terminator + fseek(File->Handle, 0, SEEK_SET); + + // TODO(matt): Consider using the MemoryArena? Maybe have separate ReadFileIntoMemory() and ReadFileIntoArena() + if(!(File->Buffer.Location = malloc(File->Buffer.Size))) + { + fclose(File->Handle); + return RC_ERROR_MEMORY; + } + File->Buffer.Ptr = File->Buffer.Location; + + fread(File->Buffer.Location, File->FileSize, 1, File->Handle); + File->Buffer.Location[File->FileSize] = '\0'; + fclose(File->Handle); + File->Buffer.ID = File->Path; + return RC_SUCCESS; +} + +size_t +StringToFletcher32(const char *Data, size_t Length) +{// https://en.wikipedia.org/wiki/Fletcher%27s_checksum + size_t c0, c1; + unsigned short int i; + + for(c0 = c1 = 0; Length >= 360; Length -= 360) + { + for(i = 0; i < 360; ++i) + { + c0 += *Data++; + c1 += c0; + } + c0 %= 65535; + c1 %= 65535; + } + + for(i = 0; i < Length; ++i) + { + c0 += *Data++; + c1 += c0; + } + + c0 %= 65535; + c1 %= 65535; + return (c1 << 16 | c0); +} + +void +ConstructAssetPath(file_buffer *AssetFile, char *Filename, int Type) +{ + buffer Path; + ClaimBuffer(&Path, "Path", Kilobytes(4)); + if(StringsDiffer(Config.RootDir, "")) + { + CopyStringToBuffer(&Path, "%s", Config.RootDir); + } + char *AssetDir = 0; + switch(Type) + { + case ASSET_CSS: AssetDir = Config.CSSDir; break; + case ASSET_IMG: AssetDir = Config.ImagesDir; break; + case ASSET_JS: AssetDir = Config.JSDir; break; + } + if(AssetDir && StringsDiffer(AssetDir, "")) { CopyStringToBuffer(&Path, "/%s", AssetDir); } + if(Filename) { CopyStringToBuffer(&Path, "/%s", Filename); } + + CopyString(AssetFile->Path, sizeof(AssetFile->Path), Path.Location); + DeclaimBuffer(&Path); +} + +void +CycleFile(file_buffer *File) +{ + fclose(File->Handle); + // TODO(matt): Rather than freeing the buffer, why not just realloc it to fit the new file? Couldn't that work easily? + // The reason we Free / Reread is to save us having to shuffle the buffer contents around, basically + // If we switch the whole database over to use linked lists - the database being the only file we actually cycle + // at the moment - then we may actually obviate the need to cycle at all + FreeBuffer(&File->Buffer); + ReadFileIntoBuffer(File, 0); +} + +void +ConstructDirectoryPath(buffer *DirectoryPath, int PageType, char *PageLocation, char *BaseFilename) +{ + RewindBuffer(DirectoryPath); + CopyStringToBuffer(DirectoryPath, "%s", Config.BaseDir); + switch(PageType) + { + case PAGE_SEARCH: + if(StringsDiffer(PageLocation, "")) + { + CopyStringToBuffer(DirectoryPath, "/%s", PageLocation); + } + break; + case PAGE_PLAYER: + if(StringsDiffer(PageLocation, "")) + { + CopyStringToBuffer(DirectoryPath, "/%s", PageLocation); + } + if(BaseFilename) + { + if(StringsDiffer(Config.PlayerURLPrefix, "")) + { + char *Ptr = BaseFilename + StringLength(Config.ProjectID); + CopyStringToBuffer(DirectoryPath, "/%s%s", Config.PlayerURLPrefix, Ptr); + } + else + { + CopyStringToBuffer(DirectoryPath, "/%s", BaseFilename); + } + } + break; + } +} + +int +ReadSearchPageIntoBuffer(file_buffer *File) +{ + buffer SearchPagePath; + ClaimBuffer(&SearchPagePath, "SearchPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + 10); + + ConstructDirectoryPath(&SearchPagePath, PAGE_SEARCH, Config.SearchLocation, 0); + CopyString(File->Path, sizeof(File->Path), "%s/index.html", SearchPagePath.Location); + DeclaimBuffer(&SearchPagePath); + return(ReadFileIntoBuffer(File, 0)); +} + +int +ReadPlayerPageIntoBuffer(file_buffer *File, db_entry *Entry) +{ + buffer PlayerPagePath; + ClaimBuffer(&PlayerPagePath, "PlayerPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10); + + ConstructDirectoryPath(&PlayerPagePath, PAGE_PLAYER, Config.PlayerLocation, Entry->BaseFilename); + CopyString(File->Path, sizeof(File->Path), "%s/index.html", PlayerPagePath.Location); + DeclaimBuffer(&PlayerPagePath); + return(ReadFileIntoBuffer(File, 0)); +} + +void +ClearTerminalRow(int Length) +{ + fprintf(stderr, "\r"); + for(int i = 0; i < Length; ++i) + { + fprintf(stderr, " "); + } + fprintf(stderr, "\r"); +} + +typedef struct +{ + uint32_t First; + uint32_t Length; +} landmark_range; + +uint32_t +GetIndexRangeLength(void *FirstLandmark, int EntryIndex, int LandmarkIndex, int LandmarkCount) +{ + uint32_t Result = 1; + db_landmark Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex); + while(EntryIndex == Landmark.EntryIndex && LandmarkIndex < LandmarkCount - 1) + { + ++LandmarkIndex; + Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex); + if(EntryIndex == Landmark.EntryIndex) + { + ++Result; + } + } + return Result; +} + +landmark_range +GetIndexRange(void *FirstLandmark, int EntryIndex, int LandmarkIndex, int LandmarkCount) +{ + landmark_range Result = {}; + db_landmark Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex); + while(EntryIndex == Landmark.EntryIndex && LandmarkIndex > 0) + { + --LandmarkIndex; + Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex); + } + if(Landmark.EntryIndex != EntryIndex) + { + ++LandmarkIndex; + } + + Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex); + + Result.First = LandmarkIndex; + Result.Length = GetIndexRangeLength(FirstLandmark, EntryIndex, LandmarkIndex, LandmarkCount); + return Result; +} + +landmark_range +BinarySearchForMetadataLandmark(void *FirstLandmark, int EntryIndex, int LandmarkCount) +{ + // NOTE(matt): Depends on FirstLandmark being positioned after an Asset "header" + landmark_range Result = {}; + if(LandmarkCount > 0) + { + int Lower = 0; + db_landmark *LowerLandmark = (db_landmark*)(FirstLandmark + sizeof(LowerLandmark) * Lower); + if(EntryIndex < LowerLandmark->EntryIndex) + { + Result.First = 0; + Result.Length = 0; + return Result; + } + + int Upper = LandmarkCount - 1; + db_landmark *UpperLandmark; + // TODO(matt): Is there a slicker way of doing this? + if(Upper >= 0) + { + UpperLandmark = (db_landmark*)(FirstLandmark + sizeof(db_landmark) * Upper); + if(EntryIndex > UpperLandmark->EntryIndex) + { + Result.First = LandmarkCount; + Result.Length = 0; + return Result; + } + } + + int Pivot = Upper - ((Upper - Lower) >> 1); + db_landmark *PivotLandmark; + + do { + LowerLandmark = (db_landmark*)(FirstLandmark + sizeof(db_landmark) * Lower); + PivotLandmark = (db_landmark*)(FirstLandmark + sizeof(db_landmark) * Pivot); + UpperLandmark = (db_landmark*)(FirstLandmark + sizeof(db_landmark) * Upper); + + if(EntryIndex == LowerLandmark->EntryIndex) + { + return GetIndexRange(FirstLandmark, EntryIndex, Lower, LandmarkCount); + } + + if(EntryIndex == PivotLandmark->EntryIndex) + { + return GetIndexRange(FirstLandmark, EntryIndex, Pivot, LandmarkCount); + } + + if(EntryIndex == UpperLandmark->EntryIndex) + { + return GetIndexRange(FirstLandmark, EntryIndex, Upper, LandmarkCount); + } + + if(EntryIndex < PivotLandmark->EntryIndex) { Upper = Pivot; } + else { Lower = Pivot; } + Pivot = Upper - ((Upper - Lower) >> 1); + } while(Upper > Pivot); + Result.First = Upper; + Result.Length = 0; + return Result; + } + return Result; +} + +void +SnipeChecksumAndCloseFile(file_buffer *File, void *FirstLandmark, int LandmarksInFile, buffer *Checksum, int *RunningLandmarkIndex) +{ + for(int j = 0; j < LandmarksInFile; ++j, ++*RunningLandmarkIndex) + { + db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(DB.Landmark) * *RunningLandmarkIndex); + + File->Buffer.Ptr = File->Buffer.Location + Landmark.Position; + CopyBufferSized(&File->Buffer, Checksum, Checksum->Ptr - Checksum->Location); + } + + File->Handle = fopen(File->Path, "w"); + fwrite(File->Buffer.Location, File->FileSize, 1, File->Handle); + fclose(File->Handle); + FreeBuffer(&File->Buffer); +} + +void +SnipeChecksumIntoHTML(void *FirstLandmark, buffer *Checksum) +{ + landmark_range SearchRange = BinarySearchForMetadataLandmark(FirstLandmark, PAGE_TYPE_SEARCH, DB.Asset.LandmarkCount); + int RunningLandmarkIndex = 0; + if(SearchRange.Length > 0) + { + file_buffer HTML; + ReadSearchPageIntoBuffer(&HTML); + SnipeChecksumAndCloseFile(&HTML, FirstLandmark, SearchRange.Length, Checksum, &RunningLandmarkIndex); + } + + for(; RunningLandmarkIndex < DB.Asset.LandmarkCount;) + { + db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * RunningLandmarkIndex); + + db_entry Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * Landmark.EntryIndex); + int Length = GetIndexRangeLength(FirstLandmark, Landmark.EntryIndex, RunningLandmarkIndex, DB.Asset.LandmarkCount); + file_buffer HTML; + ReadPlayerPageIntoBuffer(&HTML, &Entry); + SnipeChecksumAndCloseFile(&HTML, FirstLandmark, Length, Checksum, &RunningLandmarkIndex); + } +} + +void +PrintWatchHandles(void) +{ + printf("\n" + "PrintWatchHandles()\n"); + for(int i = 0; i < WatchHandles.Count; ++i) + { + printf(" %d • %s • %s\n", + WatchHandles.Handle[i].Descriptor, + WatchHandles.Handle[i].Type == WT_HMML ? "WT_HMML " : "WT_ASSET", + WatchHandles.Handle[i].Path); + } +} + +bool +IsSymlink(char *Filepath) +{ + int File = open(Filepath, O_RDONLY | O_NOFOLLOW); + bool Result = (errno == ELOOP); + close(File); + return Result; +} + +void +FitWatchHandle(void) +{ + int BlockSize = 8; + if(WatchHandles.Count == WatchHandles.Capacity) + { + WatchHandles.Capacity += BlockSize; + if(WatchHandles.Handle) + { + WatchHandles.Handle = realloc(WatchHandles.Handle, WatchHandles.Capacity * sizeof(*WatchHandles.Handle)); + } + else + { + WatchHandles.Handle = calloc(WatchHandles.Capacity, sizeof(*WatchHandles.Handle)); + } + } +} + +void +PushHMMLWatchHandle(void) +{ + FitWatchHandle(); + CopyString(WatchHandles.Handle[WatchHandles.Count].Path, sizeof(WatchHandles.Handle[0].Path), Config.ProjectDir); + WatchHandles.Handle[WatchHandles.Count].Descriptor = inotify_add_watch(inotifyInstance, Config.ProjectDir, IN_CLOSE_WRITE | IN_DELETE); + WatchHandles.Handle[WatchHandles.Count].Type = WT_HMML; + ++WatchHandles.Count; +} + +void +PushAssetWatchHandle(file_buffer *AssetFile, uint32_t AssetIndex) +{ + if(IsSymlink(AssetFile->Path)) + { + char ResolvedSymlinkPath[4096] = {}; + readlink(AssetFile->Path, ResolvedSymlinkPath, 4096); + Clear(AssetFile->Path, sizeof(AssetFile->Path)); + CopyString(AssetFile->Path, sizeof(AssetFile->Path), ResolvedSymlinkPath); + } + + ResolvePath(AssetFile->Path); + StripComponentFromPath(AssetFile->Path); + + for(int i = 0; i < WatchHandles.Count; ++i) + { + if(!StringsDiffer(WatchHandles.Handle[i].Path, AssetFile->Path)) + { + return; + } + } + + FitWatchHandle(); + WatchHandles.Handle[WatchHandles.Count].Type = WT_ASSET; + CopyString(WatchHandles.Handle[WatchHandles.Count].Path, sizeof(WatchHandles.Handle[0].Path), AssetFile->Path); + WatchHandles.Handle[WatchHandles.Count].Descriptor = inotify_add_watch(inotifyInstance, AssetFile->Path, IN_CLOSE_WRITE); + ++WatchHandles.Count; +} + +void +UpdateAssetInDB(int AssetIndex) +{ + DB.Header = *(db_header *)DB.Metadata.Buffer.Location; + if(DB.Header.HexSignature != FOURCC("CNRA")) + { + printf("line %d: Malformed .metadata file. HexSignature not in expected location\n", __LINE__); + exit(1); + } + DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header); + + DB.EntriesHeader = *(db_header_entries *)DB.Metadata.Buffer.Ptr; + if(DB.EntriesHeader.BlockID != FOURCC("NTRY")) + { + printf("line %d: Malformed .metadata file. Entries BlockID not in expected location\n", __LINE__); + exit(1); + } + + DB.Metadata.Buffer.Ptr += sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * DB.EntriesHeader.Count; + + DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr; + if(DB.AssetsHeader.BlockID != FOURCC("ASET")) + { + printf("line %d: Malformed .metadata file. Assets BlockID not in expected location\n", __LINE__); + exit(1); + } + int AssetsHeaderLocation = DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location; + DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader); + bool Found = FALSE; + for(int i = 0; i < DB.AssetsHeader.Count; ++i) + { + DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr; + if(!StringsDiffer(DB.Asset.Filename, Assets.Asset[AssetIndex].Filename) && DB.Asset.Type == Assets.Asset[AssetIndex].Type) + { + Found = TRUE; + break; + } + DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount; + } + + if(Found) + { + if(DB.Asset.Hash != Assets.Asset[AssetIndex].Hash) + { + DB.Asset.Hash = Assets.Asset[AssetIndex].Hash; + *(db_asset *)DB.Metadata.Buffer.Ptr = DB.Asset; + DB.Metadata.Buffer.Ptr += sizeof(DB.Asset); + + buffer Checksum; + ClaimBuffer(&Checksum, "Checksum", 16); + CopyStringToBuffer(&Checksum, "%08x", DB.Asset.Hash); + + file_buffer AssetFile; + ConstructAssetPath(&AssetFile, DB.Asset.Filename, DB.Asset.Type); + ResolvePath(AssetFile.Path); + + char Message[256] = { }; + CopyString(Message, sizeof(Message), "%sUpdating%s checksum %s of %s in HTML files", ColourStrings[CS_ONGOING], ColourStrings[CS_END], Checksum.Location, AssetFile.Path); + fprintf(stderr, Message); + + SnipeChecksumIntoHTML(DB.Metadata.Buffer.Ptr, &Checksum); + + ClearTerminalRow(StringLength(Message)); + fprintf(stderr, "%sUpdated%s checksum %s of %s\n", ColourStrings[CS_REINSERTION], ColourStrings[CS_END], Checksum.Location, AssetFile.Path); + + DeclaimBuffer(&Checksum); + + DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"); + fwrite(DB.Metadata.Buffer.Location, DB.Metadata.FileSize, 1, DB.Metadata.Handle); + CycleFile(&DB.Metadata); + Assets.Asset[AssetIndex].DeferredUpdate = FALSE; + } + } + else + { + // Append new asset, not bothering to insertion sort because there likely won't be many + ++DB.AssetsHeader.Count; + + DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"); + + fwrite(DB.Metadata.Buffer.Location, AssetsHeaderLocation, 1, DB.Metadata.Handle); + fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle); + fwrite(DB.Metadata.Buffer.Location + AssetsHeaderLocation + sizeof(DB.AssetsHeader), + DB.Metadata.FileSize - AssetsHeaderLocation - sizeof(DB.AssetsHeader), + 1, + DB.Metadata.Handle); + + db_asset Asset = {}; + Asset.Hash = Assets.Asset[AssetIndex].Hash; + Asset.Type = Assets.Asset[AssetIndex].Type; + ClearCopyStringNoFormat(Asset.Filename, sizeof(Asset.Filename), Assets.Asset[AssetIndex].Filename); + fwrite(&Asset, sizeof(Asset), 1, DB.Metadata.Handle); + printf("%sAppended%s %s asset: %s [%08x]\n", ColourStrings[CS_ADDITION], ColourStrings[CS_END], AssetTypeNames[Asset.Type], Asset.Filename, Asset.Hash); + + CycleFile(&DB.Metadata); + } + if(!Assets.Asset[AssetIndex].Known) + { + Assets.Asset[AssetIndex].Known = TRUE; + } +} + +void +FitAssetLandmark(enum8(builtin_assets + support_icons) AssetIndex, int PageType) +{ + int BlockSize = 2; + if(PageType == PAGE_PLAYER) + { + if(Assets.Asset[AssetIndex].PlayerLandmarkCount == Assets.Asset[AssetIndex].PlayerLandmarkCapacity) + { + Assets.Asset[AssetIndex].PlayerLandmarkCapacity += BlockSize; + if(Assets.Asset[AssetIndex].PlayerLandmark) + { + Assets.Asset[AssetIndex].PlayerLandmark = + realloc(Assets.Asset[AssetIndex].PlayerLandmark, + Assets.Asset[AssetIndex].PlayerLandmarkCapacity * sizeof(*Assets.Asset[AssetIndex].PlayerLandmark)); + } + else + { + Assets.Asset[AssetIndex].PlayerLandmark = + calloc(Assets.Asset[AssetIndex].PlayerLandmarkCapacity, sizeof(*Assets.Asset[AssetIndex].PlayerLandmark)); + } + } + } + else + { + if(Assets.Asset[AssetIndex].SearchLandmarkCount == Assets.Asset[AssetIndex].SearchLandmarkCapacity) + { + Assets.Asset[AssetIndex].SearchLandmarkCapacity += BlockSize; + if(Assets.Asset[AssetIndex].SearchLandmark) + { + Assets.Asset[AssetIndex].SearchLandmark = + realloc(Assets.Asset[AssetIndex].SearchLandmark, + Assets.Asset[AssetIndex].SearchLandmarkCapacity * sizeof(*Assets.Asset[AssetIndex].SearchLandmark)); + } + else + { + Assets.Asset[AssetIndex].SearchLandmark = + calloc(Assets.Asset[AssetIndex].SearchLandmarkCapacity, sizeof(*Assets.Asset[AssetIndex].SearchLandmark)); + } + } + } +} + +void +PushAssetLandmark(buffer *Dest, int AssetIndex, int PageType) +{ + if(!(Config.Mode & MODE_NOREVVEDRESOURCE)) + { + FitAssetLandmark(AssetIndex, PageType); + CopyStringToBuffer(Dest, "?%s=", Config.QueryString); + if(PageType == PAGE_PLAYER) + { + Assets.Asset[AssetIndex].PlayerLandmark[Assets.Asset[AssetIndex].PlayerLandmarkCount] = Dest->Ptr - Dest->Location; + ++Assets.Asset[AssetIndex].PlayerLandmarkCount; + } + else + { + Assets.Asset[AssetIndex].SearchLandmark[Assets.Asset[AssetIndex].SearchLandmarkCount] = Dest->Ptr - Dest->Location; + ++Assets.Asset[AssetIndex].SearchLandmarkCount; + } + CopyStringToBuffer(Dest, "%08x", Assets.Asset[AssetIndex].Hash); + } +} + +void +ResetAssetLandmarks(void) +{ + for(int AssetIndex = 0; AssetIndex < Assets.Count; ++AssetIndex) + { + for(int LandmarkIndex = 0; LandmarkIndex < Assets.Asset[AssetIndex].PlayerLandmarkCount; ++LandmarkIndex) + { + Assets.Asset[AssetIndex].PlayerLandmark[LandmarkIndex] = 0; + } + Assets.Asset[AssetIndex].PlayerLandmarkCount = 0; + + for(int LandmarkIndex = 0; LandmarkIndex < Assets.Asset[AssetIndex].SearchLandmarkCount; ++LandmarkIndex) + { + Assets.Asset[AssetIndex].SearchLandmark[LandmarkIndex] = 0; + } + Assets.Asset[AssetIndex].SearchLandmarkCount = 0; + Assets.Asset[AssetIndex].OffsetLandmarks = FALSE; + } +} + +void +FitAsset(void) +{ + int BlockSize = 16; + if(Assets.Count == Assets.Capacity) + { + Assets.Capacity += BlockSize; + if(Assets.Asset) + { + Assets.Asset = realloc(Assets.Asset, Assets.Capacity * sizeof(*Assets.Asset)); + } + else + { + Assets.Asset = calloc(Assets.Capacity, sizeof(*Assets.Asset)); + } + } +} + +void +UpdateAsset(uint32_t AssetIndex, bool Defer) +{ + file_buffer File; + ConstructAssetPath(&File, Assets.Asset[AssetIndex].Filename, Assets.Asset[AssetIndex].Type); + if(ReadFileIntoBuffer(&File, 0) == RC_SUCCESS) + { + Assets.Asset[AssetIndex].Hash = StringToFletcher32(File.Buffer.Location, File.FileSize); + if(!Defer) + { + UpdateAssetInDB(AssetIndex); + } + else + { + Assets.Asset[AssetIndex].DeferredUpdate = TRUE; + } + FreeBuffer(&File.Buffer); + } +} + +int +FinalPathComponentPosition(char *Path) +{ + char *Ptr = Path + StringLength(Path) - 1; + while(Ptr > Path && *Ptr != '/') + { + --Ptr; + } + if(*Ptr == '/') + { + ++Ptr; + } + return Ptr - Path; +} + +int +PlaceAsset(char *Filename, int Type, int Position) +{ + FitAsset(); + Assets.Asset[Position].Type = Type; + CopyString(Assets.Asset[Position].Filename, sizeof(Assets.Asset[0].Filename), Filename); + Assets.Asset[Position].FilenameAt = FinalPathComponentPosition(Filename); + if(Position == Assets.Count && !Assets.Asset[Position].Known) { ++Assets.Count; } + + file_buffer File; + ConstructAssetPath(&File, Filename, Type); + if(ReadFileIntoBuffer(&File, 0) == RC_SUCCESS) + { + Assets.Asset[Position].Hash = StringToFletcher32(File.Buffer.Location, File.FileSize); + FreeBuffer(&File.Buffer); + PushAssetWatchHandle(&File, Position); + return RC_SUCCESS; + } + else + { + ResolvePath(File.Path); + printf("%sNonexistent%s %s asset: %s\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], AssetTypeNames[Type], File.Path); + return RC_ERROR_FILE; + } +} + +int +PushAsset(char *Filename, int Type, uint32_t *AssetIndexPtr) +{ + for(*AssetIndexPtr = 0; *AssetIndexPtr < Assets.Count; ++*AssetIndexPtr) + { + if(!StringsDiffer(Filename, Assets.Asset[*AssetIndexPtr].Filename) && Type == Assets.Asset[*AssetIndexPtr].Type) + { + break; + } + } + return PlaceAsset(Filename, Type, *AssetIndexPtr); +} + +void +InitBuiltinAssets(void) +{ + Assert(BUILTIN_ASSETS_COUNT == ArrayCount(BuiltinAssets)); + CopyString(BuiltinAssets[ASSET_CSS_THEME].Filename, sizeof(BuiltinAssets[0].Filename), "cinera__%s.css", Config.Theme); + for(int AssetIndex = 0; AssetIndex < BUILTIN_ASSETS_COUNT; ++AssetIndex) + { + if(PlaceAsset(BuiltinAssets[AssetIndex].Filename, BuiltinAssets[AssetIndex].Type, AssetIndex) == RC_ERROR_FILE && AssetIndex == ASSET_CSS_TOPICS) + { + printf( " %s└───────┴────┴─── Don't worry about this one. We'll generate it if needed%s\n", ColourStrings[CS_COMMENT], ColourStrings[CS_END]); + } + } + Assets.Count = BUILTIN_ASSETS_COUNT; + + Assert(SUPPORT_ICON_COUNT - BUILTIN_ASSETS_COUNT == ArrayCount(SupportIcons)); + for(int SupportIconIndex = BUILTIN_ASSETS_COUNT; SupportIconIndex < SUPPORT_ICON_COUNT; ++SupportIconIndex) + { + PlaceAsset(SupportIcons[SupportIconIndex - BUILTIN_ASSETS_COUNT], ASSET_IMG, SupportIconIndex); + } +} + +void +SkipEntriesBlock(void *EntriesHeaderLocation) +{ + DB.EntriesHeader = *(db_header_entries *)EntriesHeaderLocation; + DB.Metadata.Buffer.Ptr = EntriesHeaderLocation + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * DB.EntriesHeader.Count; +} + +void +LocateAssetsBlock(void) +{ + DB.Header = *(db_header *)DB.Metadata.Buffer.Location; + DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header); + for(int BlockIndex = 0; BlockIndex < DB.Header.BlockCount; ++BlockIndex) + { + uint32_t FirstInt = *(uint32_t *)DB.Metadata.Buffer.Ptr; + if(FirstInt == FOURCC("NTRY")) + { + SkipEntriesBlock(DB.Metadata.Buffer.Ptr); + } + else if(FirstInt == FOURCC("ASET")) + { + return; + } + } + +} + +void +InitAssets(void) +{ + InitBuiltinAssets(); + LocateAssetsBlock(); + DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr; + DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader); + for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex) + { + DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr; + uint32_t AI; + PushAsset(DB.Asset.Filename, DB.Asset.Type, &AI); + DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount; + } +} + +void +ConstructResolvedAssetURL(buffer *Buffer, uint32_t AssetIndex, enum8(pages) PageType) +{ + ClaimBuffer(Buffer, "URL", (MAX_ROOT_URL_LENGTH + 1 + MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1) * 2); + ConstructURLPrefix(Buffer, Assets.Asset[AssetIndex].Type, PageType); + CopyStringToBufferHTMLPercentEncoded(Buffer, Assets.Asset[AssetIndex].Filename); + ResolvePath(Buffer->Location); +} + int SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char *Role, speakers *Speakers) { @@ -1366,7 +2537,7 @@ SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char CopyStringToBuffer(CreditsMenu, " <span class=\"credit\">\n"); - if(*Credentials[CredentialIndex].HomepageURL) + if(Credentials[CredentialIndex].HomepageURL) { CopyStringToBuffer(CreditsMenu, " <a class=\"person\" href=\"%s\" target=\"_blank\">\n" @@ -1388,17 +2559,19 @@ SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char Credentials[CredentialIndex].CreditedName); } - if(*Credentials[CredentialIndex].SupportIcon && *Credentials[CredentialIndex].SupportURL) + if(Credentials[CredentialIndex].SupportURL) { - buffer URLPrefix; - ClaimBuffer(&URLPrefix, "URLPrefix", 1024); - ConstructURLPrefix(&URLPrefix, INCLUDE_Images, PAGE_PLAYER); + buffer URL; + ConstructResolvedAssetURL(&URL, Credentials[CredentialIndex].SupportIconIndex, PAGE_PLAYER); CopyStringToBuffer(CreditsMenu, - " <a class=\"support\" href=\"%s\" target=\"_blank\"><div class=\"support_icon\" data-sprite=\"%s%s\"></div></a>\n", + " <a class=\"support\" href=\"%s\" target=\"_blank\"><div class=\"support_icon\" data-sprite=\"%s", Credentials[CredentialIndex].SupportURL, - URLPrefix.Location, - Credentials[CredentialIndex].SupportIcon); - DeclaimBuffer(&URLPrefix); + URL.Location); + DeclaimBuffer(&URL); + + PushAssetLandmark(CreditsMenu, Credentials[CredentialIndex].SupportIconIndex, PAGE_PLAYER); + + CopyStringToBuffer(CreditsMenu, "\"></div></a>\n"); } CopyStringToBuffer(CreditsMenu, @@ -1546,7 +2719,7 @@ BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Meta { if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->member, "Host", Speakers) == CreditsError_NoCredentials) { - printf("No credentials for member %s. Please contact matt@handmadedev.org with their:\n" + printf("No credentials for member %s. Please contact miblodelcarpio@gmail.com with their:\n" " Full name\n" " Homepage URL (optional)\n" " Financial support info, e.g. Patreon URL (optional)\n", Metadata->member); @@ -1559,7 +2732,7 @@ BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Meta { if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->co_hosts[i], "Co-host", Speakers) == CreditsError_NoCredentials) { - printf("No credentials for co-host %s. Please contact matt@handmadedev.org with their:\n" + printf("No credentials for co-host %s. Please contact miblodelcarpio@gmail.com with their:\n" " Full name\n" " Homepage URL (optional)\n" " Financial support info, e.g. Patreon URL (optional)\n", Metadata->co_hosts[i]); @@ -1574,7 +2747,7 @@ BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Meta { if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->guests[i], "Guest", Speakers) == CreditsError_NoCredentials) { - printf("No credentials for guest %s. Please contact matt@handmadedev.org with their:\n" + printf("No credentials for guest %s. Please contact miblodelcarpio@gmail.com with their:\n" " Full name\n" " Homepage URL (optional)\n" " Financial support info, e.g. Patreon URL (optional)\n", Metadata->guests[i]); @@ -1594,7 +2767,7 @@ BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Meta { if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->annotators[i], "Annotator", 0) == CreditsError_NoCredentials) { - printf("No credentials for annotator %s. Please contact matt@handmadedev.org with their:\n" + printf("No credentials for annotator %s. Please contact miblodelcarpio@gmail.com with their:\n" " Full name\n" " Homepage URL (optional)\n" " Financial support info, e.g. Patreon URL (optional)\n", Metadata->annotators[i]); @@ -1967,7 +3140,7 @@ CurlIntoBuffer(char *InPtr, size_t CharLength, size_t Chars, char **OutputPtr) void CurlQuotes(buffer *QuoteStaging, char *QuotesURL) { - fprintf(stderr, "\e[0;35mFetching\e[0m quotes: %s\n", QuotesURL); + fprintf(stderr, "%sFetching%s quotes: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], QuotesURL); CURL *curl = curl_easy_init(); if(curl) { CURLcode CurlReturnCode; @@ -2090,6 +3263,7 @@ BuildQuote(quote_info *Info, char *Speaker, int ID, bool ShouldFetchQuotes) if(ShouldFetchQuotes || SearchQuotes(&QuoteStaging, FileSize, Info, ID) == RC_UNFOUND) { + // TODO(matt): Error handling CurlQuotes(&QuoteStaging, QuotesURL); if(!(QuoteCache = fopen(QuoteCachePath, "w"))) @@ -2143,11 +3317,11 @@ GenerateTopicColours(char *Topic) if(StringsDiffer(Config.CSSDir, "")) { - CopyString(Topics.Path, sizeof(Topics.Path), "%s/%s/cinera_topics.css", Config.RootDir, Config.CSSDir); + CopyString(Topics.Path, sizeof(Topics.Path), "%s/%s/%s", Config.RootDir, Config.CSSDir, BuiltinAssets[ASSET_CSS_TOPICS].Filename); } else { - CopyString(Topics.Path, sizeof(Topics.Path), "%s/cinera_topics.css", Config.RootDir); + CopyString(Topics.Path, sizeof(Topics.Path), "%s/%s", Config.RootDir, BuiltinAssets[ASSET_CSS_TOPICS].Filename); } char *Ptr = Topics.Path + StringLength(Topics.Path) - 1; @@ -2222,11 +3396,19 @@ GenerateTopicColours(char *Topic) fclose(Topics.Handle); FreeBuffer(&Topics.Buffer); + if(Assets.Asset[ASSET_CSS_TOPICS].Known) + { + UpdateAsset(ASSET_CSS_TOPICS, TRUE); + } + else + { + PlaceAsset(BuiltinAssets[ASSET_CSS_TOPICS].Filename, ASSET_CSS, ASSET_CSS_TOPICS); + } return RC_SUCCESS; } else { - // NOTE(matt): Maybe it shouldn't be possible to hit this case now that we MakeDir the actually dir... + // NOTE(matt): Maybe it shouldn't be possible to hit this case now that we MakeDir the actual dir... perror(Topics.Path); return RC_ERROR_FILE; } @@ -2239,14 +3421,14 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig) "Usage: %s [option(s)] filename(s)\n" "\n" "Options:\n" - " Paths: \e[1;30m(advisedly universal, but may be set per-(sub)project as required)\e[0m\n" - " -r <root directory>\n" - " Override default root directory (\"%s\")\n" - " -R <root URL>\n" - " Override default root URL (\"%s\")\n" - " \e[1;31mIMPORTANT\e[0m: -r and -R must correspond to the same location\n" - " \e[1;30mUNSUPPORTED: If you move files from RootDir, the RootURL should\n" - " correspond to the resulting location\e[0m\n" + " Paths: %s(advisedly universal, but may be set per-(sub)project as required)%s\n" + " -r <assets root directory>\n" + " Override default assets root directory (\"%s\")\n" + " -R <assets root URL>\n" + " Override default assets root URL (\"%s\")\n" + " %sIMPORTANT%s: -r and -R must correspond to the same location\n" + " %sUNSUPPORTED: If you move files from RootDir, the RootURL should\n" + " correspond to the resulting location%s\n" "\n" " -c <CSS directory path>\n" " Override default CSS directory (\"%s\"), relative to root\n" @@ -2254,23 +3436,29 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig) " Override default images directory (\"%s\"), relative to root\n" " -j <JS directory path>\n" " Override default JS directory (\"%s\"), relative to root\n" + " -Q <revved resources query string>\n" + " Override default query string (\"%s\")\n" + " To disable revved resources, set an empty string with -Q \"\"\n" "\n" " Project Settings:\n" " -p <project ID>\n" - " Set the project ID, equal to the \"project\" field in the HMML files\n" - " NOTE: Setting the project ID triggers PROJECT EDITION\n" + " Set the project ID, triggering PROJECT EDITION\n" " -m <default medium>\n" " Override default default medium (\"%s\")\n" - " \e[1;30mKnown project defaults:\n", + " %sKnown project defaults:\n", BinaryLocation, - DefaultConfig->RootDir, + ColourStrings[CS_COMMENT], ColourStrings[CS_END], DefaultConfig->RootDir, DefaultConfig->RootURL, + ColourStrings[CS_ERROR], ColourStrings[CS_END], + ColourStrings[CS_COMMENT], ColourStrings[CS_END], DefaultConfig->CSSDir, DefaultConfig->ImagesDir, DefaultConfig->JSDir, + DefaultConfig->QueryString, - DefaultConfig->DefaultMedium); + DefaultConfig->DefaultMedium, + ColourStrings[CS_COMMENT]); for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex) { @@ -2288,7 +3476,7 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig) } fprintf(stderr, - "\e[0m -s <style>\n" + "%s -s <style>\n" " Set the style / theme, corresponding to a cinera__*.css file\n" " This is equal to the \"project\" field in the HMML files by default\n" "\n" @@ -2298,8 +3486,8 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig) " -t <templates directory>\n" " Override default templates directory (\"%s\")\n" "\n" - " -x <index template location>\n" - " Set index template file path, either absolute or relative to\n" + " -x <search template location>\n" + " Set search template file path, either absolute or relative to\n" " template directory, and enable integration\n" " -y <player template location>\n" " Set player template file path, either absolute or relative\n" @@ -2312,8 +3500,8 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig) " Override default base URL (\"%s\")\n" " NOTE: This must be set, if -n or -a are to be used\n" "\n" - " -n <index location>\n" - " Override default index location (\"%s\"), relative to base\n" + " -n <search location>\n" + " Override default search location (\"%s\"), relative to base\n" " -a <player location>\n" " Override default player location (\"%s\"), relative to base\n" " NOTE: The PlayerURLPrefix is currently hardcoded in cinera.c but\n" @@ -2334,9 +3522,9 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig) " to save us having to check their privacy status\n" " -q\n" " Quit after syncing with annotation files in project input directory\n" - " \e[1;30mUNSUPPORTED: This is likely to be removed in the future\e[0m\n" + " %sUNSUPPORTED: This is likely to be removed in the future%s\n" " -w\n" - " Force quote cache rebuild \e[1;30m(memory aid: \"wget\")\e[0m\n" + " Force quote cache rebuild %s(memory aid: \"wget\")%s\n" "\n" " -l <n>\n" " Override default log level (%d), where n is from 0 (terse) to 7 (verbose)\n" @@ -2345,19 +3533,19 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig) //" -c config location\n" "\n" " -e\n" - " Display (examine) index file and exit\n" + " Display (examine) database and exit\n" " -v\n" " Display version and exit\n" " -h\n" " Display this help\n" "\n" "Template:\n" - " A complete Index Template shall contain exactly one each of the following tags:\n" + " A valid Search Template shall contain exactly one each of the following tags:\n" " <!-- __CINERA_INCLUDES__ -->\n" " to put inside your own <head></head>\n" - " <!-- __CINERA_INDEX__ -->\n" + " <!-- __CINERA_SEARCH__ -->\n" "\n" - " A complete Player Template shall contain exactly one each of the following tags:\n" + " A valid Player Template shall contain exactly one each of the following tags:\n" " <!-- __CINERA_INCLUDES__ -->\n" " to put inside your own <head></head>\n" " <!-- __CINERA_MENUS__ -->\n" @@ -2369,12 +3557,37 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig) " <!-- __CINERA_TITLE__ -->\n" " <!-- __CINERA_VIDEO_ID__ -->\n" "\n" - " Other available tags:\n" + " Other tags available for use in either template:\n" + " Asset tags:\n" + " <!-- __CINERA_ASSET__ path.ext -->\n" + " General purpose tag that outputs the URL of the specified asset\n" + " relative to the Asset Root URL %s(-R)%s\n" + " <!-- __CINERA_IMAGE__ path.ext -->\n" + " General purpose tag that outputs the URL of the specified asset\n" + " relative to the Images Directory %s(-i)%s\n" + " <!-- __CINERA_CSS__ path.ext -->\n" + " Convenience tag that outputs a <link rel=\"stylesheet\"...> node\n" + " for the specified asset relative to the CSS Directory %s(-c)%s, for\n" + " use inside your <head> block\n" + " <!-- __CINERA_JS__ path.ext -->\n" + " Convenience tag that outputs a <script type=\"text/javascript\"...>\n" + " node for the specified asset relative to the JS Directory %s(-j)%s,\n" + " for use wherever a <script> node is valid\n" + " The path.ext in these tags supports parent directories to locate the\n" + " asset file relative to its specified type directory (generic, CSS, image\n" + " or JS), including the \"../\" directory, and paths containing spaces must\n" + " be surrounded with double-quotes (\\-escapable if the quoted path itself\n" + " contains double-quotes).\n" + "\n" + " All these asset tags additionally enable revving, appending a query\n" + " string %s(-Q)%s and the file's checksum to the URL. Changes to a file\n" + " trigger a rehash and edit of all HTML pages citing this asset.\n" + "\n" " <!-- __CINERA_PROJECT__ -->\n" " <!-- __CINERA_PROJECT_ID__ -->\n" " <!-- __CINERA_THEME__ -->\n" " <!-- __CINERA_URL__ -->\n" - " Only really usable if BaseURL is set \e[1;30m(-B)\e[0m\n" + " Only really usable if BaseURL is set %s(-B)%s\n" " <!-- __CINERA_CUSTOM0__ -->\n" " <!-- __CINERA_CUSTOM1__ -->\n" " <!-- __CINERA_CUSTOM2__ -->\n" @@ -2390,17 +3603,29 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig) "HMML Specification:\n" " https://git.handmade.network/Annotation-Pushers/Annotation-System/wikis/hmmlspec\n", + ColourStrings[CS_END], DefaultConfig->ProjectDir, DefaultConfig->TemplatesDir, DefaultConfig->BaseDir, DefaultConfig->BaseURL, - DefaultConfig->IndexLocation, + DefaultConfig->SearchLocation, DefaultConfig->PlayerLocation, DefaultConfig->OutLocation, + + ColourStrings[CS_COMMENT], ColourStrings[CS_END], + ColourStrings[CS_COMMENT], ColourStrings[CS_END], + DefaultConfig->LogLevel, - DefaultConfig->UpdateInterval); + DefaultConfig->UpdateInterval, + + ColourStrings[CS_COMMENT], ColourStrings[CS_END], + ColourStrings[CS_COMMENT], ColourStrings[CS_END], + ColourStrings[CS_COMMENT], ColourStrings[CS_END], + ColourStrings[CS_COMMENT], ColourStrings[CS_END], + ColourStrings[CS_COMMENT], ColourStrings[CS_END], + ColourStrings[CS_COMMENT], ColourStrings[CS_END]); } void @@ -2417,8 +3642,185 @@ DepartComment(buffer *Template) } } +char * +StripTrailingSlash(char *String) // NOTE(matt): For absolute paths +{ + int Length = StringLength(String); + while(Length > 0 && String[Length - 1] == '/') + { + String[Length - 1] = '\0'; + --Length; + } + return String; +} + +char * +StripSurroundingSlashes(char *String) // NOTE(matt): For relative paths +{ + char *Ptr = String; + int Length = StringLength(Ptr); + if(Length > 0) + { + while((Ptr[0]) == '/') + { + ++Ptr; + --Length; + } + while(Ptr[Length - 1] == '/') + { + Ptr[Length - 1] = '\0'; + --Length; + } + } + return Ptr; +} + +void +ConsumeWhitespace(buffer *Buffer, char **Ptr) +{ + while(*Ptr - Buffer->Location < Buffer->Size) + { + switch(**Ptr) + { + case ' ': + case '\t': + case '\n': + ++*Ptr; break; + default: return; + } + } +} + int -ValidateTemplate(template **Template, char *Location, int TemplateType) +ParseQuotedString(buffer *Dest, buffer *Src, char **Ptr, enum8(template_tag_codes) TagIndex) +{ + char *End; + bool MissingPath = FALSE; + bool MissingClosingQuote = FALSE; + if(**Ptr == '\"') + { + ++*Ptr; + End = *Ptr; + while(End - Src->Location < Src->Size && End - *Ptr < Dest->Size) + { + switch(*End) + { + case '-': + { + if((End + 2) - Src->Location < Src->Size && End[1] == '-' && End[2] == '>') + { + MissingClosingQuote = TRUE; + goto Finalise; + } + } + case '\"': goto Finalise; + case '\\': ++End; + default: *Dest->Ptr++ = *End++; break; + } + } + } + else + { + End = *Ptr; + if((End + 3) - Src->Location < Src->Size && End[0] == '-' && End[1] == '-' && End[2] == '>') + { + MissingPath = TRUE; + goto Finalise; + } + while(End - Src->Location < Src->Size && *End != ' ' && Dest->Ptr - Dest->Location < Dest->Size) + { + *Dest->Ptr++ = *End++; + } + } + *Dest->Ptr = '\0'; + +Finalise: + if(MissingClosingQuote) + { + printf("%sQuoted string%s seems to be missing its closing quote mark:\n" + " %.*s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], (int)(End - *Ptr), *Ptr); + return RC_ERROR_PARSING; + } + + if(MissingPath || StringLength(Dest->Location) == 0) + { + printf("%s%s tag%s seems to be missing a file path\n" + " %.*s\n", ColourStrings[CS_ERROR], TemplateTags[TagIndex], ColourStrings[CS_END], (int)(End - *Ptr), *Ptr); + return RC_ERROR_PARSING; + } + + if(End - *Ptr == Dest->Size) + { + printf("%sAsset file path%s is too long (we support paths up to %d characters):\n" + " %.*s...\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], MAX_ASSET_FILENAME_LENGTH, (int)(End - *Ptr), *Ptr); + return RC_ERROR_PARSING; + } + return RC_SUCCESS; +} + +void +StripPWDIndicators(char *Path) +{ + buffer B; + ClaimBuffer(&B, "PWDStrippedPtr", (StringLength(Path) + 1) * 2); + CopyStringToBufferNoFormat(&B, Path); + + B.Ptr = B.Location; + if(B.Size >= 2 && B.Ptr[0] == '.' && B.Ptr[1] == '/') + { + char *NextComponentHead = B.Ptr + 2; + int RemainingChars = StringLength(NextComponentHead); + CopyStringToBufferNoFormat(&B, NextComponentHead); + Clear(B.Ptr, B.Size - (B.Ptr - B.Location)); + B.Ptr -= RemainingChars; + } + + while(SeekBufferForString(&B, "/./", C_SEEK_FORWARDS, C_SEEK_END) == RC_SUCCESS) + { + char *NextComponentHead = B.Ptr; + int RemainingChars = StringLength(NextComponentHead); + --B.Ptr; + SeekBufferForString(&B, "/", C_SEEK_BACKWARDS, C_SEEK_START); + CopyStringToBufferNoFormat(&B, NextComponentHead); + Clear(B.Ptr, B.Size - (B.Ptr - B.Location)); + B.Ptr -= RemainingChars; + } + + CopyStringNoFormat(Path, StringLength(Path) + 1, B.Location); + DeclaimBuffer(&B); +} + +int +ParseAssetString(template **Template, enum8(template_tag_codes) TagIndex, uint32_t *AssetIndexPtr) +{ + char *Ptr = (*Template)->Buffer.Ptr + StringLength(TemplateTags[TagIndex]); + ConsumeWhitespace(&(*Template)->Buffer, &Ptr); + buffer AssetString; + ClaimBuffer(&AssetString, "AssetString", MAX_ASSET_FILENAME_LENGTH); + if(ParseQuotedString(&AssetString, &(*Template)->Buffer, &Ptr, TagIndex) == RC_ERROR_PARSING) + { + DeclaimBuffer(&AssetString); + return RC_ERROR_PARSING; + } + int AssetType = 0; + switch(TagIndex) + { + case TAG_ASSET: AssetType = ASSET_GENERIC; break; + case TAG_CSS: AssetType = ASSET_CSS; break; + case TAG_IMAGE: AssetType = ASSET_IMG; break; + case TAG_JS: AssetType = ASSET_JS; break; + } + + AssetString.Ptr = StripSurroundingSlashes(AssetString.Location); + StripPWDIndicators(AssetString.Ptr); + + PushAsset(AssetString.Ptr, AssetType, AssetIndexPtr); + DeclaimBuffer(&AssetString); + return RC_SUCCESS; +} + +int +ValidateTemplate(template **Template, char *Location, enum8(templates) TemplateType) { // TODO(matt): Record line numbers and contextual information: // <? ?> @@ -2441,12 +3843,13 @@ ValidateTemplate(template **Template, char *Location, int TemplateType) if(ClaimBuffer(&Errors, "Errors", Kilobytes(1)) == RC_ARENA_FULL) { DeclaimTemplate(*Template); return RC_ARENA_FULL; }; bool HaveErrors = FALSE; + bool HaveAssetParsingErrors = FALSE; bool FoundIncludes = FALSE; bool FoundMenus = FALSE; bool FoundPlayer = FALSE; bool FoundScript = FALSE; - bool FoundIndex = FALSE; + bool FoundSearch = FALSE; char *Previous = (*Template)->Buffer.Location; @@ -2458,9 +3861,9 @@ NextTagSearch: char *CommentStart = &(*Template)->Buffer.Ptr[-1]; while((*Template)->Buffer.Ptr - (*Template)->Buffer.Location < (*Template)->Buffer.Size && StringsDifferT("-->", (*Template)->Buffer.Ptr, 0)) { - for(int i = 0; i < ArrayCount(Tags); ++i) + for(int TagIndex = 0; TagIndex < TEMPLATE_TAG_COUNT; ++TagIndex) { - if(!(StringsDifferT(Tags[i].Tag, (*Template)->Buffer.Ptr, 0))) + if(!(StringsDifferT(TemplateTags[TagIndex], (*Template)->Buffer.Ptr, 0))) { // TODO(matt): Pack up this data for BuffersToHTML() to use /* @@ -2474,17 +3877,15 @@ NextTagSearch: * */ - int ThisTagCode = Tags[i].Code; - char *ThisTagName = Tags[i].Tag; - switch(ThisTagCode) + switch(TagIndex) { - case TAG_INDEX: - FoundIndex = TRUE; + case TAG_SEARCH: + FoundSearch = TRUE; goto RecordTag; case TAG_INCLUDES: if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundIncludes == TRUE) { - CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", ThisTagName); + CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", TemplateTags[TagIndex]); HaveErrors = TRUE; } FoundIncludes = TRUE; @@ -2492,7 +3893,7 @@ NextTagSearch: case TAG_MENUS: if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundMenus == TRUE) { - CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", Tags[i].Tag); + CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", TemplateTags[TagIndex]); HaveErrors = TRUE; } FoundMenus = TRUE; @@ -2500,7 +3901,7 @@ NextTagSearch: case TAG_PLAYER: if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundPlayer == TRUE) { - CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", Tags[i].Tag); + CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", TemplateTags[TagIndex]); HaveErrors = TRUE; } FoundPlayer = TRUE; @@ -2508,20 +3909,36 @@ NextTagSearch: case TAG_SCRIPT: if(!(Config.Mode & MODE_FORCEINTEGRATION) && (FoundMenus == FALSE || FoundPlayer == FALSE)) { - CopyStringToBuffer(&Errors, "<!-- %s --> must come after <!-- __CINERA_MENUS__ --> and <!-- __CINERA_PLAYER__ -->\n", Tags[i].Tag); + CopyStringToBuffer(&Errors, "<!-- %s --> must come after <!-- __CINERA_MENUS__ --> and <!-- __CINERA_PLAYER__ -->\n", TemplateTags[TagIndex]); HaveErrors = TRUE; } if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundScript == TRUE) { - CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", Tags[i].Tag); + CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", TemplateTags[TagIndex]); HaveErrors = TRUE; } FoundScript = TRUE; goto RecordTag; + case TAG_ASSET: + case TAG_CSS: + case TAG_IMAGE: + case TAG_JS: + { + uint32_t AI; + if(ParseAssetString(Template, TagIndex, &AI) == RC_SUCCESS) + { + (*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].AssetIndex = AI; + } + else + { + HaveErrors = TRUE; + HaveAssetParsingErrors = TRUE; + } + } goto RecordTag; default: // NOTE(matt): All freely usable tags should hit this case RecordTag: (*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].Offset = CommentStart - Previous; - (*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].TagCode = ThisTagCode; + (*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].TagCode = TagIndex; (*Template)->Metadata.TagCount++; DepartComment(&(*Template)->Buffer); Previous = (*Template)->Buffer.Ptr; @@ -2538,9 +3955,16 @@ RecordTag: } } - if(FoundIndex) + if(HaveAssetParsingErrors) { - (*Template)->Metadata.Validity |= PAGE_INDEX; + DeclaimBuffer(&Errors); + DeclaimTemplate(*Template); + return RC_INVALID_TEMPLATE; + } + + if(FoundSearch) + { + (*Template)->Metadata.Validity |= PAGE_SEARCH; } if(!HaveErrors && FoundIncludes && FoundMenus && FoundPlayer && FoundScript) @@ -2550,9 +3974,9 @@ RecordTag: if(!(Config.Mode & MODE_FORCEINTEGRATION)) { - if(TemplateType == TEMPLATE_INDEX && !((*Template)->Metadata.Validity & PAGE_INDEX)) + if(TemplateType == TEMPLATE_SEARCH && !((*Template)->Metadata.Validity & PAGE_SEARCH)) { - CopyStringToBuffer(&Errors, "Index template %s must include one <!-- __CINERA_INDEX__ --> tag\n", (*Template)->Metadata.Filename); + CopyStringToBuffer(&Errors, "Search template %s must include one <!-- __CINERA_SEARCH__ --> tag\n", (*Template)->Metadata.Filename); fprintf(stderr, "%s", Errors.Location); DeclaimBuffer(&Errors); DeclaimTemplate(*Template); @@ -2576,15 +4000,15 @@ RecordTag: } void -ConstructIndexURL(buffer *IndexURL) +ConstructSearchURL(buffer *SearchURL) { - RewindBuffer(IndexURL); + RewindBuffer(SearchURL); if(StringsDiffer(Config.BaseURL, "")) { - CopyStringToBuffer(IndexURL, "%s/", Config.BaseURL); - if(StringsDiffer(Config.IndexLocation, "")) + CopyStringToBuffer(SearchURL, "%s/", Config.BaseURL); + if(StringsDiffer(Config.SearchLocation, "")) { - CopyStringToBuffer(IndexURL, "%s/", Config.IndexLocation); + CopyStringToBuffer(SearchURL, "%s/", Config.SearchLocation); } } } @@ -2631,55 +4055,10 @@ MediumExists(char *Medium) { fprintf(stderr, " %s\n", CategoryMedium[i].Medium); } - fprintf(stderr, "To have \"%s\" added to the list, contact matt@handmadedev.org\n", Medium); + fprintf(stderr, "To have \"%s\" added to the list, contact miblodelcarpio@gmail.com\n", Medium); return FALSE; } -int -ReadFileIntoBuffer(file_buffer *File, int BufferPadding) -{ - if(!(File->Handle = fopen(File->Path, "r"))) // TODO(matt): Fuller error handling - { - return RC_ERROR_FILE; - } - - fseek(File->Handle, 0, SEEK_END); - File->FileSize = ftell(File->Handle); - File->Buffer.Size = File->FileSize + 1 + BufferPadding; // NOTE(matt): +1 to accommodate a NULL terminator - fseek(File->Handle, 0, SEEK_SET); - - // TODO(matt): Consider using the MemoryArena? Maybe have separate ReadFileIntoMemory() and ReadFileIntoArena() - if(!(File->Buffer.Location = malloc(File->Buffer.Size))) - { - fclose(File->Handle); - return RC_ERROR_MEMORY; - } - File->Buffer.Ptr = File->Buffer.Location; - - fread(File->Buffer.Location, File->FileSize, 1, File->Handle); - File->Buffer.Location[File->FileSize] = '\0'; - fclose(File->Handle); - File->Buffer.ID = File->Path; - return RC_SUCCESS; -} - -// NOTE(matt): Currently unused -int -WriteBufferToFile(file_buffer *File, buffer *Buffer, int BytesToWrite, bool KeepFileHandleOpen) -{ - if(!(File->Handle = fopen(File->Path, "w"))) - { - return RC_ERROR_FILE; - } - - fwrite(Buffer->Location, BytesToWrite, 1, File->Handle); - if(!KeepFileHandleOpen) - { - fclose(File->Handle); - } - return RC_SUCCESS; -} - typedef struct { char *BaseFilename; @@ -2692,24 +4071,77 @@ typedef struct neighbour Next; } neighbours; -int -ExamineIndex(index *Index) +void +ExamineDB1(file_buffer File) { - int IndexMetadataFileReadCode = ReadFileIntoBuffer(&Index->Metadata, 0); - switch(IndexMetadataFileReadCode) - { - case RC_ERROR_MEMORY: - return RC_ERROR_MEMORY; - case RC_ERROR_FILE: - fprintf(stderr, "Unable to open index file %s: %s\n", Index->Metadata.Path, strerror(errno)); - return RC_ERROR_FILE; - case RC_SUCCESS: - break; - } + database1 LocalDB; + LocalDB.Header = *(db_header1 *)File.Buffer.Location; + printf("Current:\n" + "\tDBVersion: %d\n" + "\tAppVersion: %d.%d.%d\n" + "\tHMMLVersion: %d.%d.%d\n" + "\n" + "Entries: %d\n", - Index->Header = *(index_header *)Index->Metadata.Buffer.Ptr; - // TODO(matt): Check that we're the current version, and maybe print out stuff accordingly? Or do we just straight up - // enforce that examining an index requires upgrading to the current version? + LocalDB.Header.DBVersion, + LocalDB.Header.AppVersion.Major, LocalDB.Header.AppVersion.Minor, LocalDB.Header.AppVersion.Patch, + LocalDB.Header.HMMLVersion.Major, LocalDB.Header.HMMLVersion.Minor, LocalDB.Header.HMMLVersion.Patch, + + LocalDB.Header.EntryCount); + + File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header); + for(int EntryIndex = 0; EntryIndex < LocalDB.Header.EntryCount; ++EntryIndex) + { + LocalDB.Entry = *(db_entry1 *)File.Buffer.Ptr; + printf(" %3d\t%s%sSize: %4d\n", + EntryIndex + 1, LocalDB.Entry.BaseFilename, + StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm + LocalDB.Entry.Size); + File.Buffer.Ptr += sizeof(LocalDB.Entry); + } +} + +void +ExamineDB2(file_buffer File) +{ + database2 LocalDB; + LocalDB.Header = *(db_header2 *)File.Buffer.Location; + printf("Current:\n" + "\tDBVersion: %d\n" + "\tAppVersion: %d.%d.%d\n" + "\tHMMLVersion: %d.%d.%d\n" + "\n" + "Relative Search Page Location: %s\n" + "Relative Player Page Location: %s\n" + "\n" + "Entries: %d\n", + + LocalDB.Header.DBVersion, + LocalDB.Header.AppVersion.Major, LocalDB.Header.AppVersion.Minor, LocalDB.Header.AppVersion.Patch, + LocalDB.Header.HMMLVersion.Major, LocalDB.Header.HMMLVersion.Minor, LocalDB.Header.HMMLVersion.Patch, + + LocalDB.Header.SearchLocation[0] != '\0' ? LocalDB.Header.SearchLocation : "(same as Base URL)", + LocalDB.Header.PlayerLocation[0] != '\0' ? LocalDB.Header.PlayerLocation : "(directly descended from Base URL)", + + LocalDB.Header.EntryCount); + + File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header); + for(int EntryIndex = 0; EntryIndex < LocalDB.Header.EntryCount; ++EntryIndex) + { + LocalDB.Entry = *(db_entry2 *)File.Buffer.Ptr; + printf(" %3d\t%s%sSize: %4d\n", + EntryIndex + 1, LocalDB.Entry.BaseFilename, + StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm + LocalDB.Entry.Size); + File.Buffer.Ptr += sizeof(LocalDB.Entry); + } +} + +void +ExamineDB3(file_buffer File) +{ + database3 LocalDB; + LocalDB.Header = *(db_header3 *)File.Buffer.Location; printf("Current:\n" "\tDBVersion: %d\n" "\tAppVersion: %d.%d.%d\n" @@ -2724,128 +4156,203 @@ ExamineIndex(index *Index) "Project Full Name: %s\n" "\n" "Base URL: %s\n" - "Relative Index Page Location: %s\n" + "Relative Search Page Location: %s\n" "Relative Player Page Location: %s\n" "Player Page URL Prefix: %s\n" "\n" "Entries: %d\n", - Index->Header.CurrentDBVersion, - Index->Header.CurrentAppVersion.Major, Index->Header.CurrentAppVersion.Minor, Index->Header.CurrentAppVersion.Patch, - Index->Header.CurrentHMMLVersion.Major, Index->Header.CurrentHMMLVersion.Minor, Index->Header.CurrentHMMLVersion.Patch, + LocalDB.Header.CurrentDBVersion, + LocalDB.Header.CurrentAppVersion.Major, LocalDB.Header.CurrentAppVersion.Minor, LocalDB.Header.CurrentAppVersion.Patch, + LocalDB.Header.CurrentHMMLVersion.Major, LocalDB.Header.CurrentHMMLVersion.Minor, LocalDB.Header.CurrentHMMLVersion.Patch, - Index->Header.InitialDBVersion, - Index->Header.InitialAppVersion.Major, Index->Header.InitialAppVersion.Minor, Index->Header.InitialAppVersion.Patch, - Index->Header.InitialHMMLVersion.Major, Index->Header.InitialHMMLVersion.Minor, Index->Header.InitialHMMLVersion.Patch, + LocalDB.Header.InitialDBVersion, + LocalDB.Header.InitialAppVersion.Major, LocalDB.Header.InitialAppVersion.Minor, LocalDB.Header.InitialAppVersion.Patch, + LocalDB.Header.InitialHMMLVersion.Major, LocalDB.Header.InitialHMMLVersion.Minor, LocalDB.Header.InitialHMMLVersion.Patch, - Index->Header.ProjectID, - Index->Header.ProjectName, + LocalDB.Header.ProjectID, + LocalDB.Header.ProjectName, - Index->Header.BaseURL, - StringsDiffer(Index->Header.IndexLocation, "") ? Index->Header.IndexLocation : "(same as Base URL)", - StringsDiffer(Index->Header.PlayerLocation, "") ? Index->Header.PlayerLocation : "(directly descended from Base URL)", - StringsDiffer(Index->Header.PlayerURLPrefix, "") ? Index->Header.PlayerURLPrefix : "(no special prefix, the player page URLs equal their entry's base filename)", + LocalDB.Header.BaseURL, + LocalDB.Header.SearchLocation[0] != '\0' ? LocalDB.Header.SearchLocation : "(same as Base URL)", + LocalDB.Header.PlayerLocation[0] != '\0' ? LocalDB.Header.PlayerLocation : "(directly descended from Base URL)", + LocalDB.Header.PlayerURLPrefix[0] != '\0' ? LocalDB.Header.PlayerURLPrefix : "(no special prefix, the player page URLs equal their entry's base filename)", - Index->Header.EntryCount); + LocalDB.Header.EntryCount); - Index->Metadata.Buffer.Ptr += sizeof(index_header); - for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex) + File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header); + for(int EntryIndex = 0; EntryIndex < LocalDB.Header.EntryCount; ++EntryIndex) { - Index->Entry = *(index_metadata *)Index->Metadata.Buffer.Ptr; + LocalDB.Entry = *(db_entry3 *)File.Buffer.Ptr; printf(" %3d\t%s%sSize: %4d\t%d\t%d\t%d\t%d\n" "\t %s\n", - EntryIndex + 1, Index->Entry.BaseFilename, - StringLength(Index->Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm - Index->Entry.Size, - Index->Entry.LinkOffsets.PrevStart, - Index->Entry.LinkOffsets.PrevEnd, - Index->Entry.LinkOffsets.NextStart, - Index->Entry.LinkOffsets.NextEnd, - Index->Entry.Title); - Index->Metadata.Buffer.Ptr += sizeof(index_metadata); + EntryIndex + 1, LocalDB.Entry.BaseFilename, + StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm + LocalDB.Entry.Size, + LocalDB.Entry.LinkOffsets.PrevStart, + LocalDB.Entry.LinkOffsets.PrevEnd, + LocalDB.Entry.LinkOffsets.NextStart, + LocalDB.Entry.LinkOffsets.NextEnd, + LocalDB.Entry.Title); + File.Buffer.Ptr += sizeof(LocalDB.Entry); } - FreeBuffer(&Index->Metadata.Buffer); - return RC_SUCCESS; } -enum +void +ExamineDB4(file_buffer File) { - C_SEEK_FORWARDS, - C_SEEK_BACKWARDS -} seek_directions; + database4 LocalDB; + LocalDB.Header = *(db_header4 *)File.Buffer.Location; -enum -{ - C_SEEK_START, // First character of string - C_SEEK_BEFORE, // Character before first character - C_SEEK_END, // Last character of string - C_SEEK_AFTER // Character after last character -} seek_positions; + LocalDB.EntriesHeader = *(db_header_entries4 *)(File.Buffer.Location + sizeof(LocalDB.Header)); + printf("Current:\n" + "\tDBVersion: %d\n" + "\tAppVersion: %d.%d.%d\n" + "\tHMMLVersion: %d.%d.%d\n" + "\n" + "Initial:\n" + "\tDBVersion: %d\n" + "\tAppVersion: %d.%d.%d\n" + "\tHMMLVersion: %d.%d.%d\n" + "\n" + "Database File Location: %s/%s.metadata\n" + "Search File Location: %s/%s.index\n" + "\n" + "Project ID: %s\n" + "Project Full Name: %s\n" + "\n" + "Base URL: %s\n" + "Relative Search Page Location: %s\n" + "Relative Player Page Location: %s\n" + "Player Page URL Prefix: %s\n" + "\n" + "Entries: %d\n", + + LocalDB.Header.CurrentDBVersion, + LocalDB.Header.CurrentAppVersion.Major, LocalDB.Header.CurrentAppVersion.Minor, LocalDB.Header.CurrentAppVersion.Patch, + LocalDB.Header.CurrentHMMLVersion.Major, LocalDB.Header.CurrentHMMLVersion.Minor, LocalDB.Header.CurrentHMMLVersion.Patch, + + LocalDB.Header.InitialDBVersion, + LocalDB.Header.InitialAppVersion.Major, LocalDB.Header.InitialAppVersion.Minor, LocalDB.Header.InitialAppVersion.Patch, + LocalDB.Header.InitialHMMLVersion.Major, LocalDB.Header.InitialHMMLVersion.Minor, LocalDB.Header.InitialHMMLVersion.Patch, + + Config.BaseDir, Config.ProjectID, + Config.BaseDir, Config.ProjectID, + + LocalDB.EntriesHeader.ProjectID, + LocalDB.EntriesHeader.ProjectName, + + LocalDB.EntriesHeader.BaseURL, + LocalDB.EntriesHeader.SearchLocation[0] != '\0' ? LocalDB.EntriesHeader.SearchLocation : "(same as Base URL)", + LocalDB.EntriesHeader.PlayerLocation[0] != '\0' ? LocalDB.EntriesHeader.PlayerLocation : "(directly descended from Base URL)", + LocalDB.EntriesHeader.PlayerURLPrefix[0] != '\0' ? LocalDB.EntriesHeader.PlayerURLPrefix : "(no special prefix, the player page URLs equal their entry's base filename)", + + LocalDB.EntriesHeader.Count); + + File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header) + sizeof(LocalDB.EntriesHeader); + for(int EntryIndex = 0; EntryIndex < LocalDB.EntriesHeader.Count; ++EntryIndex) + { + LocalDB.Entry = *(db_entry4 *)File.Buffer.Ptr; + printf(" %3d\t%s%sSize: %4d\t%d\t%d\t%d\t%d\n" + "\t %s\n", + EntryIndex, LocalDB.Entry.BaseFilename, + StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm + LocalDB.Entry.Size, + LocalDB.Entry.LinkOffsets.PrevStart, + LocalDB.Entry.LinkOffsets.PrevEnd, + LocalDB.Entry.LinkOffsets.NextStart, + LocalDB.Entry.LinkOffsets.NextEnd, + LocalDB.Entry.Title); + File.Buffer.Ptr += sizeof(LocalDB.Entry); + } + + LocalDB.AssetsHeader = *(db_header_assets4 *)File.Buffer.Ptr; + File.Buffer.Ptr += sizeof(LocalDB.AssetsHeader); + printf( "\n" + "Asset Root Directory: %s\n" + "Asset Root URL: %s\n" + " CSS Directory: %s\n" + " Images Directory: %s\n" + " JavaScript Directory: %s\n" + "Assets: %d\n" + "\n" + "%sLandmarks are displayed%s i•%sp%s %swhere i is the Entry Index and p is the Position\n" + "in bytes into the HTML file. Entry Index%s -1 %scorresponds to the Search Page%s\n", + LocalDB.AssetsHeader.RootDir, + LocalDB.AssetsHeader.RootURL, + StringsDiffer(LocalDB.AssetsHeader.CSSDir, "") ? LocalDB.AssetsHeader.CSSDir : "(same as root)", + StringsDiffer(LocalDB.AssetsHeader.ImagesDir, "") ? LocalDB.AssetsHeader.ImagesDir : "(same as root)", + StringsDiffer(LocalDB.AssetsHeader.JSDir, "") ? LocalDB.AssetsHeader.JSDir : "(same as root)", + LocalDB.AssetsHeader.Count, + ColourStrings[CS_COMMENT], ColourStrings[CS_END], + ColourStrings[CS_PURPLE], ColourStrings[CS_END], + ColourStrings[CS_COMMENT], ColourStrings[CS_END], + ColourStrings[CS_COMMENT], ColourStrings[CS_END]); + + for(int AssetIndex = 0; AssetIndex < LocalDB.AssetsHeader.Count; ++AssetIndex) + { + LocalDB.Asset = *(db_asset4*)File.Buffer.Ptr; + printf("\n" + "%s\n" + "Type: %s\n" + "Checksum: %08x\n" + "Landmarks: %d\n", + LocalDB.Asset.Filename, + AssetTypeNames[LocalDB.Asset.Type], + LocalDB.Asset.Hash, + LocalDB.Asset.LandmarkCount); + + File.Buffer.Ptr += sizeof(LocalDB.Asset); + for(int LandmarkIndex = 0; LandmarkIndex < LocalDB.Asset.LandmarkCount; ++LandmarkIndex) + { + LocalDB.Landmark = *(db_landmark *)File.Buffer.Ptr; + File.Buffer.Ptr += sizeof(LocalDB.Landmark); + printf(" %d•%s%d%s", LocalDB.Landmark.EntryIndex, ColourStrings[CS_PURPLE], LocalDB.Landmark.Position, ColourStrings[CS_END]); + } + printf("\n"); + } +} int -SeekBufferForString(buffer *Buffer, char *String, - int Direction, /* seek_directions */ - int Position /* seek_positions */) +ExamineDB(void) { - // TODO(matt): Optimise? Some means of analysing the String to increment - // the pointer in bigger strides + file_buffer DBFile; + DBFile.Buffer.ID = "DBFile"; + CopyString(DBFile.Path, sizeof(DBFile.Path), "%s/%s.metadata", Config.BaseDir, Config.ProjectID); + int DBFileReadCode = ReadFileIntoBuffer(&DBFile, 0); - // Perhaps count up runs of consecutive chars and seek for the char with - // the longest run, in strides of that run-length - - char *InitialLocation = Buffer->Ptr; - if(Direction == C_SEEK_FORWARDS) + switch(DBFileReadCode) { - while(Buffer->Ptr - Buffer->Location < Buffer->Size - StringLength(String) - && StringsDifferT(String, Buffer->Ptr, 0)) + case RC_ERROR_MEMORY: + return RC_ERROR_MEMORY; + case RC_ERROR_FILE: + fprintf(stderr, "Unable to open metadata file %s: %s\n", DBFile.Path, strerror(errno)); + return RC_ERROR_FILE; + case RC_SUCCESS: + break; + } + + uint32_t FirstInt = *(uint32_t *)DBFile.Buffer.Location; + if(FirstInt != FOURCC("CNRA")) + { + switch(FirstInt) { - ++Buffer->Ptr; + case 1: ExamineDB1(DBFile); break; + case 2: ExamineDB2(DBFile); break; + case 3: ExamineDB3(DBFile); break; + default: printf("Invalid metadata file: %s\n", DBFile.Path); break; } } else { - while(Buffer->Ptr > Buffer->Location - && StringsDifferT(String, Buffer->Ptr, 0)) + uint32_t SecondInt = *(uint32_t *)(DBFile.Buffer.Location + sizeof(uint32_t)); + switch(SecondInt) { - --Buffer->Ptr; + case 4: ExamineDB4(DBFile); break; + default: printf("Invalid metadata file: %s\n", DBFile.Path); break; } } - - if(StringsDifferT(String, Buffer->Ptr, 0)) - { - Buffer->Ptr = InitialLocation; - return RC_UNFOUND; - } - - switch(Position) - { - case C_SEEK_START: - break; - case C_SEEK_BEFORE: - if(Buffer->Ptr > Buffer->Location) - { - --Buffer->Ptr; - break; - } - else - { - return RC_ERROR_SEEK; // Ptr remains at string start - } - case C_SEEK_END: - Buffer->Ptr += StringLength(String) - 1; - break; - case C_SEEK_AFTER: - if(Buffer->Size >= Buffer->Ptr - Buffer->Location + StringLength(String)) - { - Buffer->Ptr += StringLength(String); - break; - } - else - { - return RC_ERROR_SEEK; // Ptr remains at string start - // NOTE(matt): Should it, however, be left at the end of the string? - } - } + FreeBuffer(&DBFile.Buffer); return RC_SUCCESS; } @@ -2858,23 +4365,12 @@ SeekBufferForString(buffer *Buffer, char *String, DeclaimBuffer(&QuoteMenu); \ hmml_free(&HMML); -void -ClearTerminalRow(int Length) -{ - fprintf(stderr, "\r"); - for(int i = 0; i < Length; ++i) - { - fprintf(stderr, " "); - } - fprintf(stderr, "\r"); -} - bool VideoIsPrivate(char *VideoID) { // NOTE(matt): Currently only supports YouTube char Message[128]; - CopyString(Message, sizeof(Message), "\e[0;35mChecking\e[0m privacy status of: https://youtube.com/watch?v=%s", VideoID); + CopyString(Message, sizeof(Message), "%sChecking%s privacy status of: https://youtube.com/watch?v=%s", ColourStrings[CS_ONGOING], ColourStrings[CS_END], VideoID); fprintf(stderr, "%s", Message); int MessageLength = StringLength(Message); buffer VideoAPIResponse; @@ -2925,9 +4421,12 @@ VideoIsPrivate(char *VideoID) typedef struct { - index_metadata Prev, This, Next; - bool PrevIsFirst, NextIsFinal; - short int PrevIndex, ThisIndex, NextIndex; + db_entry Prev, This, Next; + uint32_t PreLinkPrevOffsetTotal, PreLinkThisOffsetTotal, PreLinkNextOffsetTotal; + uint32_t PrevOffsetModifier, ThisOffsetModifier, NextOffsetModifier; + bool FormerIsFirst, LatterIsFinal; + bool DeletedEntryWasFirst, DeletedEntryWasFinal; + short int PrevIndex, PreDeletionThisIndex, ThisIndex, NextIndex; } neighbourhood; int @@ -2956,6 +4455,56 @@ IsCategorisedAFK(HMML_Annotation Anno) return FALSE; } +// NOTE(matt): Perhaps these OffsetLandmarks* could be made redundant / generalised once we're on the LUT +void +OffsetLandmarks(buffer *Src, int AssetIndex, int PageType) +{ + if(!(Config.Mode & MODE_NOREVVEDRESOURCE)) + { + if(PageType == PAGE_PLAYER) + { + for(int LandmarkIndex = 0; LandmarkIndex < Assets.Asset[AssetIndex].PlayerLandmarkCount; ++LandmarkIndex) + { + Assets.Asset[AssetIndex].PlayerLandmark[LandmarkIndex] += Src->Ptr - Src->Location; + } + } + else + { + for(int LandmarkIndex = 0; LandmarkIndex < Assets.Asset[AssetIndex].SearchLandmarkCount; ++LandmarkIndex) + { + Assets.Asset[AssetIndex].SearchLandmark[LandmarkIndex] += Src->Ptr - Src->Location; + } + } + } +} + +void +OffsetLandmarksIncludes(buffer *Src, int PageType) +{ + OffsetLandmarks(Src, ASSET_CSS_CINERA, PageType); + OffsetLandmarks(Src, ASSET_CSS_THEME, PageType); + OffsetLandmarks(Src, ASSET_CSS_TOPICS, PageType); + if(PageType == PAGE_PLAYER) + { + OffsetLandmarks(Src, ASSET_JS_PLAYER_PRE, PageType); + } +} + +void +OffsetLandmarksCredits(buffer *Src) +{ + OffsetLandmarks(Src, ICON_SENDOWL, PAGE_PLAYER); + OffsetLandmarks(Src, ICON_PATREON, PAGE_PLAYER); +} + +void +OffsetLandmarksMenus(buffer *Src) +{ + OffsetLandmarks(Src, ASSET_IMG_FILTER, PAGE_PLAYER); + OffsetLandmarksCredits(Src); +} +// + int HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filename, neighbourhood *N) { @@ -2963,8 +4512,7 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen RewindBuffer(&CollationBuffers->Menus); RewindBuffer(&CollationBuffers->Player); RewindBuffer(&CollationBuffers->ScriptPlayer); - RewindBuffer(&CollationBuffers->IncludesIndex); - RewindBuffer(&CollationBuffers->Search); + RewindBuffer(&CollationBuffers->SearchEntry); *CollationBuffers->Custom0 = '\0'; *CollationBuffers->Custom1 = '\0'; *CollationBuffers->Custom2 = '\0'; @@ -3004,14 +4552,14 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen HMML_Output HMML = hmml_parse_file(InFile); fclose(InFile); + char *BaseFilename = GetBaseFilename(Filename, ".hmml"); if(HMML.well_formed) { bool HaveErrors = FALSE; - char *BaseFilename = GetBaseFilename(Filename, ".hmml"); if(StringLength(BaseFilename) > MAX_BASE_FILENAME_LENGTH) { - fprintf(stderr, "\e[1;31mBase filename \"%s\" is too long (%d/%d characters)\e[0m\n", BaseFilename, StringLength(BaseFilename), MAX_BASE_FILENAME_LENGTH); + fprintf(stderr, "%sBase filename \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], BaseFilename, StringLength(BaseFilename), MAX_BASE_FILENAME_LENGTH, ColourStrings[CS_END]); HaveErrors = TRUE; } @@ -3022,7 +4570,7 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen } else if(StringLength(HMML.metadata.title) > MAX_TITLE_LENGTH) { - fprintf(stderr, "\e[1;31mVideo title \"%s\" is too long (%d/%d characters)\e[0m\n", HMML.metadata.title, StringLength(HMML.metadata.title), MAX_TITLE_LENGTH); + fprintf(stderr, "%sVideo title \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], HMML.metadata.title, StringLength(HMML.metadata.title), MAX_TITLE_LENGTH, ColourStrings[CS_END]); HaveErrors = TRUE; } else @@ -3101,7 +4649,9 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen int LengthOfString = StringLength(HMML.metadata.custom[CustomIndex]); if(LengthOfString > (CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH)) { - fprintf(stderr, "\e[1;31mCustom string %d \"\e[0m%s\e[1;31m\" is too long (%d/%d characters)\e[0m\n", CustomIndex, HMML.metadata.custom[CustomIndex], LengthOfString, CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH); + fprintf(stderr, "%sCustom string %d \"%s%s%s\" is too long (%d/%d characters)%s\n", + ColourStrings[CS_ERROR], CustomIndex, ColourStrings[CS_END], HMML.metadata.custom[CustomIndex], ColourStrings[CS_ERROR], + LengthOfString, CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH, ColourStrings[CS_END]); if(LengthOfString < MAX_CUSTOM_SNIPPET_LONG_LENGTH) { fprintf(stderr, "Consider using custom12 to custom15, which can hold %d characters\n", MAX_CUSTOM_SNIPPET_LONG_LENGTH); @@ -3156,7 +4706,7 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen if(HaveErrors) { - fprintf(stderr, "\e[1;31mSkipping\e[0m %s", BaseFilename); + fprintf(stderr, "%sSkipping%s %s", ColourStrings[CS_ERROR], ColourStrings[CS_END], BaseFilename); if(HMML.metadata.title) { fprintf(stderr, " - %s", HMML.metadata.title); } fprintf(stderr, "\n"); hmml_free(&HMML); @@ -3272,7 +4822,7 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen case CreditsError_NoHost: case CreditsError_NoAnnotator: case CreditsError_NoCredentials: - fprintf(stderr, "\e[1;31mSkipping\e[0m %s - %s\n", BaseFilename, HMML.metadata.title); + fprintf(stderr, "%sSkipping%s %s - %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], BaseFilename, HMML.metadata.title); HMMLCleanup(); return RC_ERROR_HMML; } @@ -3293,21 +4843,21 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen if(Config.Edition != EDITION_SINGLE && !PrivateVideo) { - CopyStringToBuffer(&CollationBuffers->Search, "name: \""); + CopyStringToBuffer(&CollationBuffers->SearchEntry, "name: \""); if(StringsDiffer(Config.PlayerURLPrefix, "")) { char *Ptr = BaseFilename + StringLength(Config.ProjectID); - CopyStringToBuffer(&CollationBuffers->Search, "%s%s", Config.PlayerURLPrefix, Ptr); + CopyStringToBuffer(&CollationBuffers->SearchEntry, "%s%s", Config.PlayerURLPrefix, Ptr); } else { - CopyStringToBuffer(&CollationBuffers->Search, "%s", BaseFilename); + CopyStringToBuffer(&CollationBuffers->SearchEntry, "%s", BaseFilename); } - CopyStringToBuffer(&CollationBuffers->Search, "\"\n" + CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n" "title: \""); - CopyStringToBufferNoFormat(&CollationBuffers->Search, HMML.metadata.title); - CopyStringToBuffer(&CollationBuffers->Search, "\"\n" + CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, HMML.metadata.title); + CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n" "markers:\n"); } @@ -3327,9 +4877,9 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen if(TimecodeToSeconds(Anno->time) < PreviousTimecode) { fprintf(stderr, "%s:%d: Timecode %s is chronologically before previous timecode\n" - "\e[1;31mSkipping\e[0m %s - %s\n", + "%sSkipping%s %s - %s\n", Filename, Anno->line, Anno->time, - BaseFilename, HMML.metadata.title); + ColourStrings[CS_ERROR], ColourStrings[CS_END], BaseFilename, HMML.metadata.title); HMMLCleanup(); return RC_ERROR_HMML; } @@ -3468,7 +5018,7 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen LogError(LOG_ERROR, "Reference combination processing failed: %s:%d", Filename, Anno->line); fprintf(stderr, "%s:%d: Cannot process new combination of reference info\n" "\n" - "Either tweak your annotation, or contact matt@handmadedev.org\n" + "Either tweak your annotation, or contact miblodelcarpio@gmail.com\n" "mentioning the ref node you want to write and how you want it to\n" "appear in the references menu\n", Filename, Anno->line); hmml_free(&HMML); @@ -3485,7 +5035,7 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen { if(ReferencesArray[i].IdentifierCount == REF_MAX_IDENTIFIER) { - LogError(LOG_EMERGENCY, "REF_MAX_IDENTIFIER (%d) reached. Contact matt@handmadedev.org", REF_MAX_IDENTIFIER); + LogError(LOG_EMERGENCY, "REF_MAX_IDENTIFIER (%d) reached. Contact miblodelcarpio@gmail.com", REF_MAX_IDENTIFIER); fprintf(stderr, "%s:%d: Too many timecodes associated with one reference (increase REF_MAX_IDENTIFIER)\n", Filename, Anno->line); hmml_free(&HMML); return RC_ERROR_MAX_REFS; @@ -3524,7 +5074,7 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen LogError(LOG_ERROR, "Reference combination processing failed: %s:%d", Filename, Anno->line); fprintf(stderr, "%s:%d: Cannot process new combination of reference info\n" "\n" - "Either tweak your annotation, or contact matt@handmadedev.org\n" + "Either tweak your annotation, or contact miblodelcarpio@gmail.com\n" "mentioning the ref node you want to write and how you want it to\n" "appear in the references menu\n", Filename, Anno->line); hmml_free(&HMML); @@ -3647,10 +5197,9 @@ AppendedIdentifier: Filename[StringLength(Filename) - StringLength(".hmml")] = '\0'; fprintf(stderr, "Quote #%s %d not found\n" - "\e[1;31mSkipping\e[0m %s - %s\n", - Speaker, - Anno->quote.id, - BaseFilename, HMML.metadata.title); + "%sSkipping%s %s - %s\n", + Speaker, Anno->quote.id, + ColourStrings[CS_ERROR], ColourStrings[CS_END], BaseFilename, HMML.metadata.title); hmml_free(&HMML); return RC_ERROR_QUOTE; } @@ -3693,18 +5242,18 @@ AppendedIdentifier: if(Config.Edition != EDITION_SINGLE && !PrivateVideo) { - CopyStringToBuffer(&CollationBuffers->Search, "\"%d\": \"", TimecodeToSeconds(Anno->time)); + CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"%d\": \"", TimecodeToSeconds(Anno->time)); if(Anno->is_quote && !Anno->text[0]) { - CopyStringToBuffer(&CollationBuffers->Search, "\u201C"); - CopyStringToBufferNoFormat(&CollationBuffers->Search, QuoteInfo.Text); - CopyStringToBuffer(&CollationBuffers->Search, "\u201D"); + CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201C"); + CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, QuoteInfo.Text); + CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201D"); } else { - CopyStringToBufferNoFormat(&CollationBuffers->Search, Anno->text); + CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, Anno->text); } - CopyStringToBuffer(&CollationBuffers->Search, "\"\n"); + CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n"); } while(MarkerIndex < Anno->marker_count) @@ -3818,8 +5367,8 @@ AppendedIdentifier: if(Config.Edition != EDITION_SINGLE && !PrivateVideo) { - CopyStringToBuffer(&CollationBuffers->Search, "---\n"); - N->This.Size = CollationBuffers->Search.Ptr - CollationBuffers->Search.Location; + CopyStringToBuffer(&CollationBuffers->SearchEntry, "---\n"); + N->This.Size = CollationBuffers->SearchEntry.Ptr - CollationBuffers->SearchEntry.Location; } #if DEBUG @@ -3891,17 +5440,21 @@ AppendedIdentifier: if(HasFilterMenu) { - buffer URLPrefix; - ClaimBuffer(&URLPrefix, "URLPrefix", 1024); - ConstructURLPrefix(&URLPrefix, INCLUDE_Images, PAGE_PLAYER); + buffer URL; + ConstructResolvedAssetURL(&URL, ASSET_IMG_FILTER, PAGE_PLAYER); CopyStringToBuffer(&FilterMenu, " <div class=\"menu filter\">\n" - " <span><img src=\"%scinera_icon_filter.png\"></span>\n" + " <span><img src=\"%s", + URL.Location); + DeclaimBuffer(&URL); + + PushAssetLandmark(&FilterMenu, ASSET_IMG_FILTER, PAGE_PLAYER); + + CopyStringToBuffer(&FilterMenu, + "\"></span>\n" " <div class=\"filter_container\">\n" " <div class=\"filter_mode exclusive\">Filter mode: </div>\n" - " <div class=\"filters\">\n", - URLPrefix.Location); - DeclaimBuffer(&URLPrefix); + " <div class=\"filters\">\n"); if(Topics.Count > 0) { @@ -3985,6 +5538,7 @@ AppendedIdentifier: " </div>\n" " </div>\n"); + OffsetLandmarks(&CollationBuffers->Menus, ASSET_IMG_FILTER, PAGE_PLAYER); CopyBuffer(&CollationBuffers->Menus, &FilterMenu); } @@ -4006,10 +5560,10 @@ AppendedIdentifier: if(HasCreditsMenu) { + OffsetLandmarksCredits(&CollationBuffers->Menus); CopyBuffer(&CollationBuffers->Menus, &CreditsMenu); } - // TODO(matt): Maybe figure out a more succinct way to code this Help text CopyStringToBuffer(&CollationBuffers->Menus, " <div class=\"help\">\n" " <span>?</span>\n" @@ -4020,69 +5574,23 @@ AppendedIdentifier: " <span class=\"help_key\">[</span>, <span class=\"help_key\"><</span> / <span class=\"help_key\">]</span>, <span class=\"help_key\">></span> <span class=\"help_text\">Jump to previous / next episode</span><br>\n" " <span class=\"help_key\">W</span>, <span class=\"help_key\">K</span>, <span class=\"help_key\">P</span> / <span class=\"help_key\">S</span>, <span class=\"help_key\">J</span>, <span class=\"help_key\">N</span> <span class=\"help_text\">Jump to previous / next marker</span><br>\n" " <span class=\"help_key\">t</span> / <span class=\"help_key\">T</span> <span class=\"help_text\">Toggle theatre / SUPERtheatre mode</span><br>\n" - ); + " <span class=\"help_key%s\">V</span> <span class=\"help_text%s\">Revert filter to original state</span> <span class=\"help_key\">Y</span> <span class=\"help_text\">Select link (requires manual Ctrl-c)</span>\n", - if(HasFilterMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key\">V</span> <span class=\"help_text\">Revert filter to original state</span> <span class=\"help_key\">Y</span> <span class=\"help_text\">Select link (requires manual Ctrl-c)</span>\n"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key unavailable\">z</span> <span class=\"help_text unavailable\">Toggle filter mode</span> <span class=\"help_key unavailable\">V</span> <span class=\"help_text unavailable\">Revert filter to original state</span>\n"); - } + HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable"); CopyStringToBuffer(&CollationBuffers->Menus, "\n" - " <h2>Menu toggling</h2>\n"); + " <h2>Menu toggling</h2>\n" + " <span class=\"help_key%s\">q</span> <span class=\"help_text%s\">Quotes</span>\n" + " <span class=\"help_key%s\">r</span> <span class=\"help_text%s\">References</span>\n" + " <span class=\"help_key%s\">f</span> <span class=\"help_text%s\">Filter</span>\n" + " <span class=\"help_key\">y</span> <span class=\"help_text\">Link</span>\n" + " <span class=\"help_key%s\">c</span> <span class=\"help_text%s\">Credits</span>\n", - if(HasQuoteMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key\">q</span> <span class=\"help_text\">Quotes</span>\n"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key unavailable\">q</span> <span class=\"help_text unavailable\">Quotes</span>\n"); - } - - if(HasReferenceMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key\">r</span> <span class=\"help_text\">References</span>\n"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key unavailable\">r</span> <span class=\"help_text unavailable\">References</span>\n"); - } - - if(HasFilterMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key\">f</span> <span class=\"help_text\">Filter</span>\n"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key unavailable\">f</span> <span class=\"help_text unavailable\">Filter</span>\n"); - } - - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key\">y</span> <span class=\"help_text\">Link</span>\n"); - - if(HasCreditsMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key\">c</span> <span class=\"help_text\">Credits</span>\n"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key unavailable\">c</span> <span class=\"help_text unavailable\">Credits</span>\n"); - } + HasQuoteMenu ? "" : " unavailable", HasQuoteMenu ? "" : " unavailable", + HasReferenceMenu ? "" : " unavailable", HasReferenceMenu ? "" : " unavailable", + HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", + HasCreditsMenu ? "" : " unavailable", HasCreditsMenu ? "" : " unavailable"); CopyStringToBuffer(&CollationBuffers->Menus, "\n" @@ -4121,158 +5629,73 @@ AppendedIdentifier: " </div>\n" " <br>\n"); - if(HasQuoteMenu) - { CopyStringToBuffer(&CollationBuffers->Menus, - " <h2>Quotes "); - if(HasReferenceMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, "and References Menus</h2>\n"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, "<span class=\"unavailable\">and References</span> Menus</h2>\n"); - } - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <h2><span class=\"unavailable\">Quotes"); - if(HasReferenceMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, " and</span> References Menus</h2>\n"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, " and References Menus</span></h2>\n"); - } - } + " <h2>%sQuotes %sand%s References%s Menus%s</h2>\n" + " <span class=\"help_key word%s\">Enter</span> <span class=\"help_text%s\">Jump to timecode</span><br>\n", + // Q R + // + // 0 0 <h2><span off>Quotes and References Menus</span></h2> + // 0 1 <h2><span off>Quotes and</span> References Menus</h2> + // 1 0 <h2>Quotes <span off>and References</span> Menus</h2> + // 1 1 <h2>Quotes and References Menus</h2> - if(HasQuoteMenu || HasReferenceMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key word\">Enter</span> <span class=\"help_text\">Jump to timecode</span><br>\n"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key word unavailable\">Enter</span> <span class=\"help_text unavailable\">Jump to timecode</span><br>\n"); - } - - CopyStringToBuffer(&CollationBuffers->Menus, "\n"); - - if(HasQuoteMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <h2>Quotes"); - if(HasReferenceMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, ", References "); - if(HasCreditsMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus</h2>"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, "<span class=\"unavailable\">and Credits</span> Menus</h2>"); - } - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, "<span class=\"unavailable\">, References "); - if(HasCreditsMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, "</span>and Credits Menus</h2>"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, "and Credits</span> Menus</h2>"); - } - } - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <h2><span class=\"unavailable\">Quotes"); - if(HasReferenceMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, ", </span>References "); - if(HasCreditsMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus</h2>"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, "<span class=\"unavailable\">and Credits</span> Menus</h2>"); - } - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, ", References "); - if(HasCreditsMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, "and</span> Credits Menus</h2>"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus</h2></span>"); - } - } - } - - CopyStringToBuffer(&CollationBuffers->Menus, "\n"); - - if(HasQuoteMenu || HasReferenceMenu || HasCreditsMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key\">o</span> <span class=\"help_text\">Open URL (in new tab)</span>\n"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <span class=\"help_key unavailable\">o</span> <span class=\"help_text unavailable\">Open URL (in new tab)</span>\n"); - } + HasQuoteMenu ? "" : "<span class=\"unavailable\">", + HasQuoteMenu && !HasReferenceMenu ? "<span class=\"unavailable\">" : "", + !HasQuoteMenu && HasReferenceMenu ? "</span>" : "", + HasQuoteMenu && !HasReferenceMenu ? "</span>" : "", + !HasQuoteMenu && !HasReferenceMenu ? "</span>" : "", + HasQuoteMenu || HasReferenceMenu ? "" : " unavailable", HasQuoteMenu || HasReferenceMenu ? "" : " unavailable"); CopyStringToBuffer(&CollationBuffers->Menus, - "\n"); + "\n" + " <h2>%sQuotes%s,%s References %sand%s Credits%s Menus%s</h2>" + " <span class=\"help_key%s\">o</span> <span class=\"help_text%s\">Open URL (in new tab)</span>\n", + // Q R C + // + // 0 0 0 <h2><span off>Quotes, References and Credits Menus</span></h2> + // 0 0 1 <h2><span off>Quotes, References and</span> Credits Menus</h2> + // 0 1 0 <h2><span off>Quotes,</span> References <span off>and Credits</span> Menus</h2> + // 0 1 1 <h2><span off>Quotes,</span> References and Credits Menus</h2> + // 1 0 0 <h2>Quotes<span off>, References and Credits</span> Menus</h2> + // 1 0 1 <h2>Quotes<span off>, References </span>and Credits Menus</h2> + // 1 1 0 <h2>Quotes, References <span off>and Credits</span> Menus</h2> + // 1 1 1 <h2>Quotes, References and Credits Menus</h2> - if(HasFilterMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <h2>Filter Menu</h2>\n" - " <span class=\"help_key\">x</span>, <span class=\"help_key word\">Space</span> <span class=\"help_text\">Toggle category and focus next</span><br>\n" - " <span class=\"help_key\">X</span>, <span class=\"help_key word modifier\">Shift</span><span class=\"help_key word\">Space</span> <span class=\"help_text\">Toggle category and focus previous</span><br>\n" - " <span class=\"help_key\">v</span> <span class=\"help_text\">Invert topics / media as per focus</span>\n" - "\n" - " <h2>Filter and Link Menus</h2>\n" - " <span class=\"help_key\">z</span> <span class=\"help_text\">Toggle filter / linking mode</span>\n"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <h2><span class=\"unavailable\">Filter Menu</span></h2>\n" - " <span class=\"help_key unavailable\">x</span>, <span class=\"help_key word unavailable\">Space</span> <span class=\"help_text unavailable\">Toggle category and focus next</span><br>\n" - " <span class=\"help_key unavailable\">X</span>, <span class=\"help_key word modifier unavailable\">Shift</span><span class=\"help_key word unavailable\">Space</span> <span class=\"help_text unavailable\">Toggle category and focus previous</span><br>\n" - " <span class=\"help_key unavailable\">v</span> <span class=\"help_text unavailable\">Invert topics / media as per focus</span>\n" - "\n" - " <h2><span class=\"unavailable\">Filter</span> and Link Menus</h2>\n" - " <span class=\"help_key\">z</span> <span class=\"help_text\">Toggle <span class=\"unavailable\">filter /</span> linking mode</span>\n"); - } + /* 0 */ HasQuoteMenu ? "" : "<span class=\"unavailable\">", + /* 1 */ HasQuoteMenu && !HasReferenceMenu ? "<span class=\"unavailable\">" : "", + /* 2 */ !HasQuoteMenu && HasReferenceMenu ? "</span>" : "", + /* 3 */ HasReferenceMenu && !HasCreditsMenu ? "<span class=\"unavailable\">" : HasQuoteMenu && !HasReferenceMenu && HasCreditsMenu ? "</span>" : "", + /* 4 */ !HasQuoteMenu && !HasReferenceMenu && HasCreditsMenu ? "</span>" : "", + /* 5 */ !HasCreditsMenu && (HasQuoteMenu || HasReferenceMenu) ? "</span>" : "", + /* 6 */ !HasQuoteMenu && !HasReferenceMenu && !HasCreditsMenu ? "</span>" : "", - CopyStringToBuffer(&CollationBuffers->Menus, "\n"); + HasQuoteMenu || HasReferenceMenu || HasCreditsMenu ? "" : " unavailable", + HasQuoteMenu || HasReferenceMenu || HasCreditsMenu ? "" : " unavailable"); - if(HasCreditsMenu) - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <h2>Credits Menu</h2>\n" - " <span class=\"help_key word\">Enter</span> <span class=\"help_text\">Open URL (in new tab)</span><br>\n"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Menus, - " <h2><span class=\"unavailable\">Credits Menu</span></h2>\n" - " <span class=\"help_key word unavailable\">Enter</span> <span class=\"help_text unavailable\">Open URL (in new tab)</span><br>\n"); - } + CopyStringToBuffer(&CollationBuffers->Menus, + "\n" + " <h2>%sFilter Menu%s</h2>\n" + " <span class=\"help_key%s\">x</span>, <span class=\"help_key word%s\">Space</span> <span class=\"help_text%s\">Toggle category and focus next</span><br>\n" + " <span class=\"help_key%s\">X</span>, <span class=\"help_key word modifier%s\">Shift</span><span class=\"help_key word%s\">Space</span> <span class=\"help_text%s\">Toggle category and focus previous</span><br>\n" + " <span class=\"help_key%s\">v</span> <span class=\"help_text%s\">Invert topics / media as per focus</span>\n" + "\n" + " <h2>%sFilter and%s Link Menus</h2>\n" + " <span class=\"help_key\">z</span> <span class=\"help_text\">Toggle %sfilter /%s linking mode</span>\n", + + HasFilterMenu ? "" : "<span class=\"unavailable\">", HasFilterMenu ? "" : "</span>", + HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", + HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", + HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", + HasFilterMenu ? "" : "<span class=\"unavailable\">", HasFilterMenu ? "" : "</span>", + HasFilterMenu ? "" : "<span class=\"help_text unavailable\">", HasFilterMenu ? "" : "</span>"); + + CopyStringToBuffer(&CollationBuffers->Menus, + "\n" + " <h2>%sCredits Menu%s</h2>\n" + " <span class=\"help_key word%s\">Enter</span> <span class=\"help_text%s\">Open URL (in new tab)</span><br>\n", + + HasCreditsMenu ? "" : "<span class=\"unavailable\">", HasCreditsMenu ? "" : "</span>", + HasCreditsMenu ? "" : " unavailable", HasCreditsMenu ? "" : " unavailable"); CopyStringToBuffer(&CollationBuffers->Menus, " </div>\n" @@ -4316,29 +5739,11 @@ AppendedIdentifier: // TODO(matt): Maybe do something about indentation levels - buffer URLIndex; - ClaimBuffer(&URLIndex, "URLIndex", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1); - ConstructIndexURL(&URLIndex); - CopyString(CollationBuffers->URLIndex, sizeof(CollationBuffers->URLIndex), "%s", URLIndex.Location); - DeclaimBuffer(&URLIndex); - - buffer URLPrefix; - ClaimBuffer(&URLPrefix, "URLPrefix", 1024); - ConstructURLPrefix(&URLPrefix, INCLUDE_CSS, PAGE_INDEX); - CopyStringToBuffer(&CollationBuffers->IncludesIndex, - "<meta charset=\"UTF-8\">\n" - " <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n" - "\n" - " <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera.css\">\n" - " <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera__%s.css\">\n" - " <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera_topics.css\">\n", - CINERA_APP_VERSION.Major, - CINERA_APP_VERSION.Minor, - CINERA_APP_VERSION.Patch, - - URLPrefix.Location, - URLPrefix.Location, StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project, - URLPrefix.Location); + buffer URLSearch; + ClaimBuffer(&URLSearch, "URLSearch", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1); + ConstructSearchURL(&URLSearch); + CopyString(CollationBuffers->URLSearch, sizeof(CollationBuffers->URLSearch), "%s", URLSearch.Location); + DeclaimBuffer(&URLSearch); CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "<meta charset=\"UTF-8\">\n" @@ -4375,25 +5780,54 @@ AppendedIdentifier: CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "\">\n"); } - ConstructURLPrefix(&URLPrefix, INCLUDE_CSS, PAGE_PLAYER); - CopyStringToBuffer(&CollationBuffers->IncludesPlayer, - "\n" - " <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera.css\">\n" - " <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera__%s.css\">\n" - " <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera_topics.css\">\n", - URLPrefix.Location, - URLPrefix.Location, StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project, - URLPrefix.Location); + buffer URL; - ConstructURLPrefix(&URLPrefix, INCLUDE_JS, PAGE_PLAYER); + ConstructResolvedAssetURL(&URL, ASSET_CSS_CINERA, PAGE_PLAYER); CopyStringToBuffer(&CollationBuffers->IncludesPlayer, - " <script type=\"text/javascript\" src=\"%scinera_player_pre.js\"></script>", - URLPrefix.Location); + "\n" + " <link rel=\"stylesheet\" type=\"text/css\" href=\"%s", + URL.Location); + DeclaimBuffer(&URL); + PushAssetLandmark(&CollationBuffers->IncludesPlayer, ASSET_CSS_CINERA, PAGE_PLAYER); + + ConstructResolvedAssetURL(&URL, ASSET_CSS_THEME, PAGE_PLAYER); + CopyStringToBuffer(&CollationBuffers->IncludesPlayer, + "\">\n" + " <link rel=\"stylesheet\" type=\"text/css\" href=\"%s", + URL.Location); + DeclaimBuffer(&URL); + PushAssetLandmark(&CollationBuffers->IncludesPlayer, ASSET_CSS_THEME, PAGE_PLAYER); + + ConstructResolvedAssetURL(&URL, ASSET_CSS_TOPICS, PAGE_PLAYER); + CopyStringToBuffer(&CollationBuffers->IncludesPlayer, + "\">\n" + " <link rel=\"stylesheet\" type=\"text/css\" href=\"%s", + URL.Location); + DeclaimBuffer(&URL); + PushAssetLandmark(&CollationBuffers->IncludesPlayer, ASSET_CSS_TOPICS, PAGE_PLAYER); + + CopyStringToBuffer(&CollationBuffers->IncludesPlayer, + "\">\n"); + + ConstructResolvedAssetURL(&URL, ASSET_JS_PLAYER_PRE, PAGE_PLAYER); + CopyStringToBuffer(&CollationBuffers->IncludesPlayer, + " <script type=\"text/javascript\" src=\"%s", + URL.Location); + DeclaimBuffer(&URL); + PushAssetLandmark(&CollationBuffers->IncludesPlayer, ASSET_JS_PLAYER_PRE, PAGE_PLAYER); + + CopyStringToBuffer(&CollationBuffers->IncludesPlayer, + "\"></script>"); + + ConstructResolvedAssetURL(&URL, ASSET_JS_PLAYER_POST, PAGE_PLAYER); + CopyStringToBuffer(&CollationBuffers->ScriptPlayer, + "<script type=\"text/javascript\" src=\"%s", + URL.Location); + DeclaimBuffer(&URL); + PushAssetLandmark(&CollationBuffers->ScriptPlayer, ASSET_JS_PLAYER_POST, PAGE_PLAYER); CopyStringToBuffer(&CollationBuffers->ScriptPlayer, - "<script type=\"text/javascript\" src=\"%scinera_player_post.js\"></script>", - URLPrefix.Location); - DeclaimBuffer(&URLPrefix); + "\"></script>"); // NOTE(matt): Tree structure of "global" buffer dependencies // CreditsMenu @@ -4413,7 +5847,7 @@ AppendedIdentifier: else { LogError(LOG_ERROR, "%s:%d: %s", Filename, HMML.error.line, HMML.error.message); - fprintf(stderr, "\e[1;31mSkipping\e[0m %s:%d: %s\n", Filename, HMML.error.line, HMML.error.message); + fprintf(stderr, "%sSkipping%s %s:%d: %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], Filename, HMML.error.line, HMML.error.message); hmml_free(&HMML); return RC_ERROR_HMML; } @@ -4509,35 +5943,39 @@ BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, i } else { - CopyStringToBufferNoFormat(&Output, CollationBuffers->URLIndex); + CopyStringToBufferNoFormat(&Output, CollationBuffers->URLSearch); } break; case TAG_VIDEO_ID: CopyStringToBufferNoFormat(&Output, CollationBuffers->VideoID); break; - case TAG_INDEX: + case TAG_SEARCH: if(Config.Edition == EDITION_SINGLE) { - fprintf(stderr, "Template contains a <!-- __CINERA_INDEX__ --> tag\n" - "Skipping just this tag, because an index cannot be generated for inclusion in a\n" + fprintf(stderr, "Template contains a <!-- __CINERA_SEARCH__ --> tag\n" + "Skipping just this tag, because a search cannot be generated for inclusion in a\n" "bespoke template in Single Edition\n"); } else { - CopyBuffer(&Output, &CollationBuffers->Index); + OffsetLandmarks(&Output, ASSET_JS_SEARCH, PAGE_SEARCH); + CopyBuffer(&Output, &CollationBuffers->Search); } break; case TAG_INCLUDES: if(PageType == PAGE_PLAYER) { + OffsetLandmarksIncludes(&Output, PageType); CopyBuffer(&Output, &CollationBuffers->IncludesPlayer); } else { - CopyBuffer(&Output, &CollationBuffers->IncludesIndex); + OffsetLandmarksIncludes(&Output, PageType); + CopyBuffer(&Output, &CollationBuffers->IncludesSearch); } break; case TAG_MENUS: + OffsetLandmarksMenus(&Output); CopyBuffer(&Output, &CollationBuffers->Menus); break; case TAG_PLAYER: @@ -4545,8 +5983,47 @@ BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, i CopyBuffer(&Output, &CollationBuffers->Player); break; case TAG_SCRIPT: + OffsetLandmarks(&Output, ASSET_JS_PLAYER_POST, PAGE_PLAYER); CopyBuffer(&Output, &CollationBuffers->ScriptPlayer); break; + case TAG_ASSET: + { + buffer URL; + ConstructResolvedAssetURL(&URL, Template->Metadata.Tag[i].AssetIndex, PageType); + CopyStringToBuffer(&Output, "%s", URL.Location); + DeclaimBuffer(&URL); + PushAssetLandmark(&Output, Template->Metadata.Tag[i].AssetIndex, PageType); + } break; + case TAG_CSS: + { + buffer URL; + ConstructResolvedAssetURL(&URL, Template->Metadata.Tag[i].AssetIndex, PageType); + CopyStringToBuffer(&Output, + "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s", + URL.Location); + DeclaimBuffer(&URL); + PushAssetLandmark(&Output, Template->Metadata.Tag[i].AssetIndex, PageType); + CopyStringToBuffer(&Output, "\">"); + } break; + case TAG_IMAGE: + { + buffer URL; + ConstructResolvedAssetURL(&URL, Template->Metadata.Tag[i].AssetIndex, PageType); + CopyStringToBuffer(&Output, "%s", URL.Location); + DeclaimBuffer(&URL); + PushAssetLandmark(&Output, Template->Metadata.Tag[i].AssetIndex, PageType); + } break; + case TAG_JS: + { + buffer URL; + ConstructResolvedAssetURL(&URL, Template->Metadata.Tag[i].AssetIndex, PageType); + CopyStringToBuffer(&Output, + "<script type=\"text/javascript\" src=\"%s", + URL.Location); + DeclaimBuffer(&URL); + PushAssetLandmark(&Output, Template->Metadata.Tag[i].AssetIndex, PageType); + CopyStringToBuffer(&Output, "\"></script>"); + } break; case TAG_CUSTOM0: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom0); break; case TAG_CUSTOM1: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom1); break; case TAG_CUSTOM2: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom2); break; @@ -4616,7 +6093,8 @@ BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, i " <head>\n" " "); - CopyBuffer(&Master, PageType == PAGE_PLAYER ? &CollationBuffers->IncludesPlayer : &CollationBuffers->IncludesIndex); + OffsetLandmarksIncludes(&Master, PageType); + CopyBuffer(&Master, PageType == PAGE_PLAYER ? &CollationBuffers->IncludesPlayer : &CollationBuffers->IncludesSearch); CopyStringToBuffer(&Master, "\n"); CopyStringToBuffer(&Master, @@ -4627,6 +6105,8 @@ BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, i { CopyStringToBuffer(&Master, "<div>\n" " "); + + OffsetLandmarksMenus(&Master); CopyBuffer(&Master, &CollationBuffers->Menus); CopyStringToBuffer(&Master, "\n" " "); @@ -4638,12 +6118,15 @@ BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, i " "); CopyStringToBuffer(&Master, "</div>\n" " "); + + OffsetLandmarks(&Master, ASSET_JS_PLAYER_POST, PAGE_PLAYER); CopyBuffer(&Master, &CollationBuffers->ScriptPlayer); CopyStringToBuffer(&Master, "\n"); } else { - CopyBuffer(&Master, &CollationBuffers->Index); + OffsetLandmarks(&Master, ASSET_JS_SEARCH, PAGE_SEARCH); + CopyBuffer(&Master, &CollationBuffers->Search); } CopyStringToBuffer(&Master, @@ -4665,21 +6148,22 @@ BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, i } int -BinarySearchForMetadataEntry(index *Index, index_metadata **Entry, char *SearchTerm) +BinarySearchForMetadataEntry(db_entry **Entry, char *SearchTerm) { + DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader); int Lower = 0; - index_metadata *LowerEntry = (index_metadata*)(Index->Metadata.Buffer.Ptr + sizeof(index_metadata) * Lower); + db_entry *LowerEntry = (db_entry*)(DB.Metadata.Buffer.Ptr + sizeof(DB.Entry) * Lower); if(StringsDiffer(SearchTerm, LowerEntry->BaseFilename) < 0 ) { return -1; } - int Upper = Index->Header.EntryCount - 1; + int Upper = DB.EntriesHeader.Count - 1; int Pivot = Upper - ((Upper - Lower) >> 1); - index_metadata *UpperEntry; - index_metadata *PivotEntry; + db_entry *UpperEntry; + db_entry *PivotEntry; do { - LowerEntry = (index_metadata*)(Index->Metadata.Buffer.Ptr + sizeof(index_metadata) * Lower); - PivotEntry = (index_metadata*)(Index->Metadata.Buffer.Ptr + sizeof(index_metadata) * Pivot); - UpperEntry = (index_metadata*)(Index->Metadata.Buffer.Ptr + sizeof(index_metadata) * Upper); + LowerEntry = (db_entry*)(DB.Metadata.Buffer.Ptr + sizeof(DB.Entry) * Lower); + PivotEntry = (db_entry*)(DB.Metadata.Buffer.Ptr + sizeof(DB.Entry) * Pivot); + UpperEntry = (db_entry*)(DB.Metadata.Buffer.Ptr + sizeof(DB.Entry) * Upper); if(!StringsDiffer(SearchTerm, LowerEntry->BaseFilename)) { *Entry = LowerEntry; return Lower; } if(!StringsDiffer(SearchTerm, PivotEntry->BaseFilename)) { *Entry = PivotEntry; return Pivot; } @@ -4693,34 +6177,33 @@ BinarySearchForMetadataEntry(index *Index, index_metadata **Entry, char *SearchT } int -AccumulateIndexEntryInsertionOffset(index *Index, int EntryIndex) +AccumulateDBEntryInsertionOffset(int EntryIndex) { int Result = 0; - index_metadata *AccEntry = { 0 }; - if(EntryIndex < Index->Header.EntryCount >> 1) + db_entry *AccEntry = { 0 }; + if(EntryIndex < DB.EntriesHeader.Count >> 1) { - //printf("\u200B"); // NOTE(matt): Don't ask... for(; EntryIndex > 0; --EntryIndex) { - AccEntry = (index_metadata*)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * (EntryIndex - 1)); + AccEntry = (db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * (EntryIndex - 1)); Result += AccEntry->Size; } Result += StringLength("---\n"); } else { - for(; EntryIndex < Index->Header.EntryCount; ++EntryIndex) + for(; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex) { - AccEntry = (index_metadata*)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex); + AccEntry = (db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex); Result += AccEntry->Size; } - Result = Index->File.FileSize - Result; + Result = DB.File.FileSize - Result; } return Result; } void -ClearEntry(index_metadata *Entry) +ClearEntry(db_entry *Entry) { Entry->LinkOffsets.PrevStart = 0; Entry->LinkOffsets.NextStart = 0; @@ -4731,105 +6214,133 @@ ClearEntry(index_metadata *Entry) Clear(Entry->Title, sizeof(Entry->Title)); } -enum +void +InitNeighbourhood(neighbourhood *N) { - EDIT_DELETION, - EDIT_ADDITION, - EDIT_REINSERTION -} index_edits; + N->PrevIndex = -1; + N->PreDeletionThisIndex = N->ThisIndex = 0; + N->NextIndex = -1; + + N->PreLinkPrevOffsetTotal = N->PreLinkThisOffsetTotal = N->PreLinkNextOffsetTotal = 0; + N->PrevOffsetModifier = N->ThisOffsetModifier = N->NextOffsetModifier = 0; + N->FormerIsFirst = N->LatterIsFinal = FALSE; + N->DeletedEntryWasFirst = N->DeletedEntryWasFinal = FALSE; + + N->Prev.LinkOffsets.PrevStart = N->Prev.LinkOffsets.PrevEnd = N->Prev.LinkOffsets.NextStart = N->Prev.LinkOffsets.NextEnd = 0; + N->This.LinkOffsets.PrevStart = N->This.LinkOffsets.PrevEnd = N->This.LinkOffsets.NextStart = N->This.LinkOffsets.NextEnd = 0; + N->Next.LinkOffsets.PrevStart = N->Next.LinkOffsets.PrevEnd = N->Next.LinkOffsets.NextStart = N->Next.LinkOffsets.NextEnd = 0; + + N->Prev.Size = N->This.Size = N->Next.Size = 0; + + Clear(N->Prev.BaseFilename, sizeof(N->Prev.BaseFilename)); + Clear(N->This.BaseFilename, sizeof(N->This.BaseFilename)); + Clear(N->Next.BaseFilename, sizeof(N->Next.BaseFilename)); + + Clear(N->Prev.Title, sizeof(N->Prev.Title)); + Clear(N->This.Title, sizeof(N->This.Title)); + Clear(N->Next.Title, sizeof(N->Next.Title)); +} + +#define PrintNeighbourhood(N) PrintNeighbourhood_(N, __LINE__) +void +PrintNeighbourhood_(neighbourhood *N, int Line) +{ + printf( "\n" + " Neighbourhood (line %d):\n", Line); + + if(N->PrevIndex >= 0) + { + printf( + " Prev [%d]: %s: %6d %6d %6d %6d\n", + N->PrevIndex, N->Prev.BaseFilename, + N->Prev.LinkOffsets.PrevStart, + N->Prev.LinkOffsets.PrevEnd, + N->Prev.LinkOffsets.NextStart, + N->Prev.LinkOffsets.NextEnd); + } + + if(N->ThisIndex >= 0) + { + printf( + " This [%d (pre-deletion %d)]: %s: %6d %6d %6d %6d\n", + N->ThisIndex, N->PreDeletionThisIndex, N->This.BaseFilename, + N->This.LinkOffsets.PrevStart, + N->This.LinkOffsets.PrevEnd, + N->This.LinkOffsets.NextStart, + N->This.LinkOffsets.NextEnd); + } + + if(N->NextIndex >= 0) + { + printf( + " Next [%d]: %s: %6d %6d %6d %6d\n", + N->NextIndex, N->Next.BaseFilename, + N->Next.LinkOffsets.PrevStart, + N->Next.LinkOffsets.PrevEnd, + N->Next.LinkOffsets.NextStart, + N->Next.LinkOffsets.NextEnd); + } + + printf( + " OffsetModifiers: Prev %6d • This %6d • Next %6d\n" + " PreLinkOffsetTotals: Prev %6d • This %6d • Next %6d\n" + " DeletedEntryWasFirst: %s\n" + " DeletedEntryWasFinal: %s\n" + " FormerIsFirst: %s\n" + " LatterIsFinal: %s\n" + "\n", + N->PrevOffsetModifier, N->ThisOffsetModifier, N->NextOffsetModifier, + N->PreLinkPrevOffsetTotal, N->PreLinkThisOffsetTotal, N->PreLinkNextOffsetTotal, + N->DeletedEntryWasFirst ? "yes" : "no", N->DeletedEntryWasFinal ? "yes" : "no", + N->FormerIsFirst ? "yes" : "no", N->LatterIsFinal ? "yes" : "no"); +} void -GetNeighbourhood(index *Index, neighbourhood *N, int IndexEditType, bool *ThisIsPrev) +SetNeighbour(db_entry *Dest, db_entry *Src) { - index_metadata Entry = { }; + Dest->Size = Src->Size; + Dest->LinkOffsets.PrevStart = Src->LinkOffsets.PrevStart; + Dest->LinkOffsets.PrevEnd = Src->LinkOffsets.PrevEnd; + Dest->LinkOffsets.NextStart = Src->LinkOffsets.NextStart; + Dest->LinkOffsets.NextEnd = Src->LinkOffsets.NextEnd; + CopyString(Dest->BaseFilename, sizeof(Dest->BaseFilename), "%s", Src->BaseFilename); + CopyString(Dest->Title, sizeof(Dest->Title), "%s", Src->Title); +} + +void +GetNeighbourhoodForAddition(neighbourhood *N, enum8(edit_types) EditType) +{ + db_entry Entry = { }; + int EntryIndex; - int IncomingIndex = N->ThisIndex; - if(IndexEditType == EDIT_DELETION) - { - bool FoundThis = FALSE; - for(EntryIndex = IncomingIndex + 1; EntryIndex < Index->Header.EntryCount; ++EntryIndex) - { - Entry = *(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex); - if(Entry.Size > 0) - { - FoundThis = TRUE; - break; - } - } - - if(!FoundThis) - { - for(EntryIndex = IncomingIndex - 1; EntryIndex >= 0; --EntryIndex) - { - Entry = *(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex); - if(Entry.Size > 0) - { - FoundThis = TRUE; - *ThisIsPrev = TRUE; - break; - } - } - } - - if(FoundThis) - { - N->ThisIndex = EntryIndex; - N->This.Size = Entry.Size; - N->This.LinkOffsets.PrevStart = Entry.LinkOffsets.PrevStart; - N->This.LinkOffsets.PrevEnd = Entry.LinkOffsets.PrevEnd; - N->This.LinkOffsets.NextStart = Entry.LinkOffsets.NextStart; - N->This.LinkOffsets.NextEnd = Entry.LinkOffsets.NextEnd; - CopyString(N->This.BaseFilename, sizeof(N->This.BaseFilename), "%s", Entry.BaseFilename); - CopyString(N->This.Title, sizeof(N->This.Title), "%s", Entry.Title); - } - else - { - return; // NOTE(matt): We were evidently the last public entry, until now - } - } - - N->PrevIsFirst = TRUE; - N->NextIsFinal = TRUE; - - if(IndexEditType == EDIT_DELETION && *ThisIsPrev == FALSE) - { - EntryIndex = IncomingIndex - 1; - } - else - { - EntryIndex = N->ThisIndex - 1; - } bool FoundPrev = FALSE; + bool FoundNext = FALSE; + + N->FormerIsFirst = TRUE; + EntryIndex = N->ThisIndex - 1; + for(; EntryIndex >= 0; --EntryIndex) { - Entry = *(index_metadata*)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex); + Entry = *(db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex); if(Entry.Size > 0) { if(!FoundPrev) { N->PrevIndex = EntryIndex; - N->Prev.Size = Entry.Size; - N->Prev.LinkOffsets.PrevStart = Entry.LinkOffsets.PrevStart; - N->Prev.LinkOffsets.PrevEnd = Entry.LinkOffsets.PrevEnd; - N->Prev.LinkOffsets.NextStart = Entry.LinkOffsets.NextStart; - N->Prev.LinkOffsets.NextEnd = Entry.LinkOffsets.NextEnd; - CopyString(N->Prev.BaseFilename, sizeof(N->Prev.BaseFilename), "%s", Entry.BaseFilename); - CopyString(N->Prev.Title, sizeof(N->Prev.Title), "%s", Entry.Title); + SetNeighbour(&N->Prev, &Entry); FoundPrev = TRUE; } else { - N->PrevIsFirst = FALSE; + N->FormerIsFirst = FALSE; break; } } } - switch(IndexEditType) + switch(EditType) { - case EDIT_DELETION: - if(*ThisIsPrev == TRUE) { EntryIndex = Index->Header.EntryCount; break; } // NOTE(matt): No need to enter the loop, else fallthrough case EDIT_REINSERTION: EntryIndex = N->ThisIndex + 1; break; @@ -4837,142 +6348,149 @@ GetNeighbourhood(index *Index, neighbourhood *N, int IndexEditType, bool *ThisIs EntryIndex = N->ThisIndex; break; } - bool FoundNext = FALSE; - for(; EntryIndex < Index->Header.EntryCount; + + N->LatterIsFinal = TRUE; + for(; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex) { - Entry = *(index_metadata*)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex); + Entry = *(db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex); if(Entry.Size > 0) { if(!FoundNext) { N->NextIndex = EntryIndex; - N->Next.Size = Entry.Size; - N->Next.LinkOffsets.PrevStart = Entry.LinkOffsets.PrevStart; - N->Next.LinkOffsets.PrevEnd = Entry.LinkOffsets.PrevEnd; - N->Next.LinkOffsets.NextStart = Entry.LinkOffsets.NextStart; - N->Next.LinkOffsets.NextEnd = Entry.LinkOffsets.NextEnd; - CopyString(N->Next.BaseFilename, sizeof(N->Next.BaseFilename), "%s", Entry.BaseFilename); - CopyString(N->Next.Title, sizeof(N->Next.Title), "%s", Entry.Title); + SetNeighbour(&N->Next, &Entry); FoundNext = TRUE; } else { - N->NextIsFinal = FALSE; + N->LatterIsFinal = FALSE; break; } } } - switch(IndexEditType) + if(EditType == EDIT_ADDITION && FoundNext) { - case EDIT_REINSERTION: - break; - case EDIT_DELETION: - if(*ThisIsPrev == FALSE) { --N->ThisIndex; } - if(FoundNext) { --N->NextIndex; } - break; - case EDIT_ADDITION: - if(FoundNext) { ++N->NextIndex; } - break; + ++N->NextIndex; } } -int -InsertIntoIndex(index *Index, neighbourhood *N, buffers *CollationBuffers, template **BespokeTemplate, char *BaseFilename, bool RecheckingPrivacy) +void +GetNeighbourhoodForDeletion(neighbourhood *N) { - int IndexEntryInsertionStart = -1; - int IndexEntryInsertionEnd = -1; - Index->Header.EntryCount = 0; - ClearEntry(&Index->Entry); - bool Reinserting = FALSE; + db_entry Entry = { }; + Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * N->ThisIndex); + N->PreDeletionThisIndex = N->ThisIndex; + SetNeighbour(&N->This, &Entry); - index_metadata *Entry = { 0 }; - if(Index->Metadata.FileSize > 0 && Index->File.FileSize > 0) + int EntryIndex; + + N->DeletedEntryWasFirst = TRUE; + N->FormerIsFirst = TRUE; + bool FoundPrev = FALSE; + for(EntryIndex = N->PreDeletionThisIndex - 1; EntryIndex >= 0; --EntryIndex) { - // TODO(matt): Index validation? - // Maybe at least if(!StringsDiffer(..., "name: \""); - // and check that we won't crash through the end of the file when skipping to the next entry + Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex); + if(Entry.Size > 0) + { + if(!FoundPrev) + { + FoundPrev = TRUE; + N->DeletedEntryWasFirst = FALSE; - Index->Header = *(index_header *)Index->Metadata.Buffer.Location; - Index->Header.CurrentDBVersion = CINERA_DB_VERSION; - Index->Header.CurrentAppVersion = CINERA_APP_VERSION; - Index->Header.CurrentHMMLVersion.Major = hmml_version.Major; - Index->Header.CurrentHMMLVersion.Minor = hmml_version.Minor; - Index->Header.CurrentHMMLVersion.Patch = hmml_version.Patch; + N->PrevIndex = EntryIndex; + SetNeighbour(&N->Prev, &Entry); + } + else + { + N->FormerIsFirst = FALSE; + break; + } + } + } - *(index_header *)Index->Metadata.Buffer.Location = Index->Header; - Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + sizeof(Index->Header); - Index->File.Buffer.Ptr = Index->File.Buffer.Location + StringLength("---\n"); + N->DeletedEntryWasFinal = TRUE; + N->LatterIsFinal = TRUE; + bool FoundNext = FALSE; + for(EntryIndex = N->PreDeletionThisIndex + 1; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex) + { + Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex); + if(Entry.Size > 0) + { + if(!FoundNext) + { + FoundNext = TRUE; + N->DeletedEntryWasFinal = FALSE; - N->ThisIndex = BinarySearchForMetadataEntry(Index, &Entry, BaseFilename); + N->NextIndex = EntryIndex - 1; + SetNeighbour(&N->Next, &Entry); + } + else + { + N->LatterIsFinal = FALSE; + break; + } + } + } +} + +void +GetNeighbourhood(neighbourhood *N, enum8(edit_types) EditType) +{ + if(EditType == EDIT_DELETION) + { + GetNeighbourhoodForDeletion(N); + } + else + { + GetNeighbourhoodForAddition(N, EditType); + } +} + +void +SnipeEntryIntoMetadataBuffer(db_entry *Entry, int EntryIndex) +{ + *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex) = *Entry; +} + +int +InsertIntoDB(neighbourhood *N, buffers *CollationBuffers, template **BespokeTemplate, char *BaseFilename, bool RecheckingPrivacy, bool *Reinserting) +{ + enum8(edit_types) EditType = EDIT_APPEND; + int EntryInsertionStart = StringLength("---\n"); + int EntryInsertionEnd; + + if(DB.EntriesHeader.Count > 0) + { + db_entry *Entry = { 0 }; + N->ThisIndex = BinarySearchForMetadataEntry(&Entry, BaseFilename); if(Entry) { // Reinsert - Reinserting = TRUE; - N->This.Size = Entry->Size; - N->This.LinkOffsets.PrevStart = Entry->LinkOffsets.PrevStart; - N->This.LinkOffsets.PrevEnd = Entry->LinkOffsets.PrevEnd; - N->This.LinkOffsets.NextStart = Entry->LinkOffsets.NextStart; - N->This.LinkOffsets.NextEnd = Entry->LinkOffsets.NextEnd; - - IndexEntryInsertionStart = AccumulateIndexEntryInsertionOffset(Index, N->ThisIndex); - IndexEntryInsertionEnd = IndexEntryInsertionStart + Entry->Size; + *Reinserting = TRUE; + EntryInsertionStart = AccumulateDBEntryInsertionOffset(N->ThisIndex); + EntryInsertionEnd = EntryInsertionStart + Entry->Size; + EditType = EDIT_REINSERTION; } else { if(N->ThisIndex == -1) { ++N->ThisIndex; } // NOTE(matt): BinarySearchForMetadataEntry returns -1 if search term precedes the set - Entry = (index_metadata*)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * N->ThisIndex); + Entry = (db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * N->ThisIndex); if(StringsDiffer(BaseFilename, Entry->BaseFilename) < 0) { // Insert - IndexEntryInsertionStart = AccumulateIndexEntryInsertionOffset(Index, N->ThisIndex); - + EditType = EDIT_INSERTION; } else { // Append ++N->ThisIndex; + EditType = EDIT_APPEND; } + EntryInsertionStart = AccumulateDBEntryInsertionOffset(N->ThisIndex); } - GetNeighbourhood(Index, N, Reinserting ? EDIT_REINSERTION : EDIT_ADDITION, 0); - } - else - { - // NOTE(matt): Initialising new index_header - Index->Header.InitialDBVersion = Index->Header.CurrentDBVersion = CINERA_DB_VERSION; - Index->Header.InitialAppVersion = Index->Header.CurrentAppVersion = CINERA_APP_VERSION; - Index->Header.InitialHMMLVersion.Major = Index->Header.CurrentHMMLVersion.Major = hmml_version.Major; - Index->Header.InitialHMMLVersion.Minor = Index->Header.CurrentHMMLVersion.Minor = hmml_version.Minor; - Index->Header.InitialHMMLVersion.Patch = Index->Header.CurrentHMMLVersion.Patch = hmml_version.Patch; - - CopyStringNoFormat(Index->Header.ProjectID, sizeof(Index->Header.ProjectID), Config.ProjectID); - - for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex) - { - if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID)) - { - CopyStringNoFormat(Index->Header.ProjectName, sizeof(Index->Header.ProjectName), ProjectInfo[ProjectIndex].FullName); - break; - } - } - - CopyStringNoFormat(Index->Header.BaseURL, sizeof(Index->Header.BaseURL), Config.BaseURL); - CopyStringNoFormat(Index->Header.IndexLocation, sizeof(Index->Header.IndexLocation), Config.IndexLocation); - CopyStringNoFormat(Index->Header.PlayerLocation, sizeof(Index->Header.PlayerLocation), Config.PlayerLocation); - CopyStringNoFormat(Index->Header.PlayerURLPrefix, sizeof(Index->Header.PlayerURLPrefix), Config.PlayerURLPrefix); - - DIR *OutputDirectoryHandle; - if(!(OutputDirectoryHandle = opendir(Config.BaseDir))) - { - if(MakeDir(Config.BaseDir) == RC_ERROR_DIRECTORY) - { - LogError(LOG_ERROR, "Unable to create directory %s: %s", Config.BaseDir, strerror(errno)); - fprintf(stderr, "Unable to create directory %s: %s\n", Config.BaseDir, strerror(errno)); - return RC_ERROR_DIRECTORY; - }; - } - closedir(OutputDirectoryHandle); + GetNeighbourhood(N, *Reinserting ? EDIT_REINSERTION : EDIT_ADDITION); } char InputFile[StringLength(BaseFilename) + StringLength(".hmml") + 1]; @@ -4997,105 +6515,82 @@ InsertIntoIndex(index *Index, neighbourhood *N, buffers *CollationBuffers, templ } ClearCopyStringNoFormat(N->This.BaseFilename, sizeof(N->This.BaseFilename), BaseFilename); - if(N->This.Size > 0) { ClearCopyStringNoFormat(N->This.Title, sizeof(N->This.Title), CollationBuffers->Title); } + if(!VideoIsPrivate) { ClearCopyStringNoFormat(N->This.Title, sizeof(N->This.Title), CollationBuffers->Title); } - if(Reinserting) + if(EditType == EDIT_REINSERTION) { - // Reinsert + // NOTE(matt): To save opening the DB.Metadata file, we defer sniping N->This in until InsertNeighbourLink() if(!VideoIsPrivate) { - if(!(Index->File.Handle = fopen(Index->File.Path, "w"))) { return RC_ERROR_FILE; } - fwrite(Index->File.Buffer.Location, IndexEntryInsertionStart, 1, Index->File.Handle); - fwrite(CollationBuffers->Search.Location, N->This.Size, 1, Index->File.Handle); - fwrite(Index->File.Buffer.Location + IndexEntryInsertionEnd, Index->File.FileSize - IndexEntryInsertionEnd, 1, Index->File.Handle); - fclose(Index->File.Handle); + if(!(DB.File.Handle = fopen(DB.File.Path, "w"))) { return RC_ERROR_FILE; } + fwrite(DB.File.Buffer.Location, EntryInsertionStart, 1, DB.File.Handle); + fwrite(CollationBuffers->SearchEntry.Location, N->This.Size, 1, DB.File.Handle); + fwrite(DB.File.Buffer.Location + EntryInsertionEnd, DB.File.FileSize - EntryInsertionEnd, 1, DB.File.Handle); + fclose(DB.File.Handle); - if(N->This.Size == IndexEntryInsertionEnd - IndexEntryInsertionStart) + if(N->This.Size == EntryInsertionEnd - EntryInsertionStart) { - Index->File.Buffer.Ptr = Index->File.Buffer.Location + IndexEntryInsertionStart; - CopyBufferSized(&Index->File.Buffer, &CollationBuffers->Search, N->This.Size); + DB.File.Buffer.Ptr = DB.File.Buffer.Location + EntryInsertionStart; + CopyBufferSized(&DB.File.Buffer, &CollationBuffers->SearchEntry, N->This.Size); } else { - FreeBuffer(&Index->File.Buffer); - ReadFileIntoBuffer(&Index->File, 0); + FreeBuffer(&DB.File.Buffer); + ReadFileIntoBuffer(&DB.File, 0); } - LogError(LOG_NOTICE, "Reinserted %s - %s", BaseFilename, CollationBuffers->Title); - fprintf(stderr, "\e[1;33mReinserted\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title); - } - else if(!RecheckingPrivacy) - { - LogError(LOG_NOTICE, "Privately Reinserted %s", BaseFilename); - fprintf(stderr, "\e[0;34mPrivately Reinserted\e[0m %s\n", BaseFilename); } } else { - ++Index->Header.EntryCount; + int ExistingEntryCount = DB.EntriesHeader.Count; + ++DB.EntriesHeader.Count; - if(!(Index->Metadata.Handle = fopen(Index->Metadata.Path, "w"))) { return RC_ERROR_FILE; } - fwrite(&Index->Header, sizeof(index_header), 1, Index->Metadata.Handle); + if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { return RC_ERROR_FILE; } + fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle); + fwrite(&DB.EntriesHeader, sizeof(DB.EntriesHeader), 1, DB.Metadata.Handle); - if(!(Index->File.Handle = fopen(Index->File.Path, "w"))) { return RC_ERROR_FILE; } + DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader); + fwrite(DB.Metadata.Buffer.Ptr, + sizeof(DB.Entry), + N->ThisIndex, + DB.Metadata.Handle); + DB.Metadata.Buffer.Ptr += sizeof(DB.Entry) * N->ThisIndex; - if(IndexEntryInsertionStart >= 0) - { - // Insert new - fwrite(Index->Metadata.Buffer.Location + sizeof(index_header), sizeof(index_metadata) * N->ThisIndex, 1, Index->Metadata.Handle); - fwrite(&N->This, sizeof(index_metadata), 1, Index->Metadata.Handle); - fwrite(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * N->ThisIndex, - Index->Metadata.FileSize - sizeof(index_header) - sizeof(index_metadata) * N->ThisIndex, - 1, Index->Metadata.Handle); + fwrite(&N->This, sizeof(DB.Entry), 1, DB.Metadata.Handle); - fwrite(Index->File.Buffer.Location, IndexEntryInsertionStart, 1, Index->File.Handle); - fwrite(CollationBuffers->Search.Location, N->This.Size, 1, Index->File.Handle); - fwrite(Index->File.Buffer.Location + IndexEntryInsertionStart, Index->File.FileSize - IndexEntryInsertionStart, 1, Index->File.Handle); + fwrite(DB.Metadata.Buffer.Ptr, + sizeof(DB.Entry), + ExistingEntryCount - N->ThisIndex, + DB.Metadata.Handle); + DB.Metadata.Buffer.Ptr += sizeof(DB.Entry) * (ExistingEntryCount - N->ThisIndex); - if(!VideoIsPrivate) - { - LogError(LOG_NOTICE, "Inserted %s - %s", BaseFilename, CollationBuffers->Title); - fprintf(stderr, "\e[1;32mInserted\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title); - } - else if(!RecheckingPrivacy) - { - LogError(LOG_NOTICE, "Privately Inserted %s", BaseFilename); - fprintf(stderr, "\e[0;34mPrivately Inserted\e[0m %s\n", BaseFilename); - } - } - else - { - // Append new - if(Index->Metadata.FileSize > 0) - { - fwrite(Index->Metadata.Buffer.Location + sizeof(index_header), Index->Metadata.FileSize - sizeof(index_header), 1, Index->Metadata.Handle); - fwrite(Index->File.Buffer.Location, Index->File.FileSize, 1, Index->File.Handle); - } - else - { - fprintf(Index->File.Handle, "---\n"); - } - fwrite(&N->This, sizeof(index_metadata), 1, Index->Metadata.Handle); - fwrite(CollationBuffers->Search.Location, N->This.Size, 1, Index->File.Handle); + fwrite(DB.Metadata.Buffer.Ptr, + DB.Metadata.FileSize - (DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location), + 1, + DB.Metadata.Handle); - if(!VideoIsPrivate) - { - LogError(LOG_NOTICE, "Appended %s - %s", BaseFilename, CollationBuffers->Title); - fprintf(stderr, "\e[1;32mAppended\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title); - } - else if(!RecheckingPrivacy) - { - LogError(LOG_NOTICE, "Privately Appended %s", BaseFilename); - fprintf(stderr, "\e[0;34mPrivately Appended\e[0m %s\n", BaseFilename); - } - } + CycleFile(&DB.Metadata); - fclose(Index->Metadata.Handle); - FreeBuffer(&Index->Metadata.Buffer); - ReadFileIntoBuffer(&Index->Metadata, 0); - fclose(Index->File.Handle); - FreeBuffer(&Index->File.Buffer); - ReadFileIntoBuffer(&Index->File, 0); + if(!(DB.File.Handle = fopen(DB.File.Path, "w"))) { return RC_ERROR_FILE; } + fwrite(DB.File.Buffer.Location, + DB.File.FileSize - (DB.File.FileSize - EntryInsertionStart), + 1, DB.File.Handle); + fwrite(CollationBuffers->SearchEntry.Location, N->This.Size, 1, DB.File.Handle); + fwrite(DB.File.Buffer.Location + EntryInsertionStart, DB.File.FileSize - EntryInsertionStart, 1, DB.File.Handle); + + CycleFile(&DB.File); + } + + if(!VideoIsPrivate) + { + LogError(LOG_NOTICE, "%s %s - %s", EditTypes[EditType].Name, BaseFilename, CollationBuffers->Title); + fprintf(stderr, "%s%s%s %s - %s\n", ColourStrings[EditTypes[EditType].Colour], EditTypes[EditType].Name, ColourStrings[CS_END], BaseFilename, CollationBuffers->Title); + } + else if(!RecheckingPrivacy) + { + LogError(LOG_NOTICE, "Privately %s %s", EditTypes[EditType].Name, BaseFilename); + fprintf(stderr, "%sPrivately %s%s %s\n", ColourStrings[CS_PRIVATE], EditTypes[EditType].Name, ColourStrings[CS_END], BaseFilename); } // TODO(matt): Remove VideoIsPrivate in favour of generating a player page in a random location @@ -5103,36 +6598,357 @@ InsertIntoIndex(index *Index, neighbourhood *N, buffers *CollationBuffers, templ } void -ConstructDirectoryPath(buffer *DirectoryPath, int PageType, char *PageLocation, char *BaseFilename) +WritePastAssetsHeader(void) { - RewindBuffer(DirectoryPath); - CopyStringToBuffer(DirectoryPath, "%s", Config.BaseDir); - switch(PageType) + DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"); + fwrite(DB.Metadata.Buffer.Location, + sizeof(DB.Header) + + sizeof(DB.EntriesHeader) + + sizeof(DB.Entry) * DB.EntriesHeader.Count + + sizeof(DB.AssetsHeader), + 1, + DB.Metadata.Handle); + + DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + + sizeof(DB.Header) + + sizeof(DB.EntriesHeader) + + sizeof(DB.Entry) * DB.EntriesHeader.Count; + + DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr; + DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader); +} + +void +PrintLandmarks(void *FirstLandmark, int LandmarkCount) +{ + printf("PrintLandmarks()\n"); + for(int i = 0; i < LandmarkCount; ++i) { - case PAGE_INDEX: - if(StringsDiffer(PageLocation, "")) + db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * i); + printf(" %4d %d\n", Landmark.EntryIndex, Landmark.Position); + } +} + +void +ProcessPrevLandmarks(neighbourhood *N, void *FirstLandmark, int ExistingLandmarkCount, landmark_range *CurrentTarget, bool OffsetLandmarks, int *RunningIndex) +{ + if(N->PrevIndex >= 0) + { + landmark_range FormerTarget = BinarySearchForMetadataLandmark(FirstLandmark, N->PrevIndex, ExistingLandmarkCount); + fwrite(FirstLandmark, sizeof(db_landmark), FormerTarget.First, DB.Metadata.Handle); + *RunningIndex += FormerTarget.First; + + for(int j = 0; j < FormerTarget.Length; ++j, ++*RunningIndex) + { + db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * *RunningIndex); + if(!OffsetLandmarks && Landmark.Position >= N->PreLinkPrevOffsetTotal) { - CopyStringToBuffer(DirectoryPath, "/%s", PageLocation); + Landmark.Position += N->PrevOffsetModifier; } - break; - case PAGE_PLAYER: - if(StringsDiffer(PageLocation, "")) + fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.Handle); + } + } + else + { + fwrite(FirstLandmark + sizeof(db_landmark) * *RunningIndex, sizeof(db_landmark), CurrentTarget->First, DB.Metadata.Handle); + *RunningIndex += CurrentTarget->First; + } +} + +void +ProcessNextLandmarks(neighbourhood *N, void *FirstLandmark, int ExistingLandmarkCount, bool OffsetLandmarks, int *RunningIndex, enum8(edit_types) EditType) +{ + if(N->NextIndex >= 0 && *RunningIndex < ExistingLandmarkCount) + { + db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * *RunningIndex); + landmark_range LatterTarget = BinarySearchForMetadataLandmark(FirstLandmark, Landmark.EntryIndex, ExistingLandmarkCount); + + for(int j = 0; j < LatterTarget.Length; ++j, ++*RunningIndex) + { + Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * *RunningIndex); + if(!OffsetLandmarks && Landmark.Position >= N->PreLinkNextOffsetTotal) { - CopyStringToBuffer(DirectoryPath, "/%s", PageLocation); + Landmark.Position += N->NextOffsetModifier; } - if(BaseFilename) + switch(EditType) { - if(StringsDiffer(Config.PlayerURLPrefix, "")) + case EDIT_DELETION: --Landmark.EntryIndex; break; + case EDIT_ADDITION: ++Landmark.EntryIndex; break; + } + fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.Handle); + } + + for(; *RunningIndex < ExistingLandmarkCount; ++*RunningIndex) + { + Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * *RunningIndex); + switch(EditType) + { + case EDIT_DELETION: --Landmark.EntryIndex; break; + case EDIT_ADDITION: ++Landmark.EntryIndex; break; + } + fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.Handle); + } + } +} + +void +DeleteStaleAssets(void) +{ + LocateAssetsBlock(); + char *AssetsHeaderLocation = DB.Metadata.Buffer.Ptr; + DB.AssetsHeader = *(db_header_assets *)AssetsHeaderLocation; + int AssetDeletionCount = 0; + int AssetDeletionLocations[DB.AssetsHeader.Count]; + DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader); + for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex) + { + DB.Asset = *(db_asset*)DB.Metadata.Buffer.Ptr; + if(DB.Asset.LandmarkCount == 0) + { + AssetDeletionLocations[AssetDeletionCount] = DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location; + ++AssetDeletionCount; + --DB.AssetsHeader.Count; + } + DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount; + } + + if(AssetDeletionCount > 0) + { + DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"); + fwrite(DB.Metadata.Buffer.Location, AssetsHeaderLocation - DB.Metadata.Buffer.Location, 1, DB.Metadata.Handle); + fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle); + + int WrittenBytes = (AssetsHeaderLocation - DB.Metadata.Buffer.Location) + sizeof(DB.AssetsHeader); + + for(int DeletionIndex = 0; DeletionIndex < AssetDeletionCount; ++DeletionIndex) + { + DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + AssetDeletionLocations[DeletionIndex]; + fwrite(DB.Metadata.Buffer.Location + WrittenBytes, + (DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location) - WrittenBytes, + 1, + DB.Metadata.Handle); + WrittenBytes += (DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location) - WrittenBytes + sizeof(DB.Asset); + } + fwrite(DB.Metadata.Buffer.Location + WrittenBytes, DB.Metadata.FileSize - WrittenBytes, 1, DB.Metadata.Handle); + CycleFile(&DB.Metadata); + } +} + +void +DeleteStaleLandmarks(void) +{ + WritePastAssetsHeader(); + for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex) + { + DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr; + DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount; + DB.Asset.LandmarkCount = 0; + fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle); + } + CycleFile(&DB.Metadata); +} + +void +DeleteLandmarks(neighbourhood *N) +{ + for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex) + { + DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr; + DB.Metadata.Buffer.Ptr += sizeof(DB.Asset); + db_landmark *FirstLandmark = (db_landmark *)DB.Metadata.Buffer.Ptr; + int ExistingLandmarkCount = DB.Asset.LandmarkCount; + int RunningIndex = 0; + + landmark_range DeletionTarget = BinarySearchForMetadataLandmark(FirstLandmark, N->PreDeletionThisIndex, ExistingLandmarkCount); + DB.Asset.LandmarkCount -= DeletionTarget.Length; + fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle); + + ProcessPrevLandmarks(N, FirstLandmark, ExistingLandmarkCount, &DeletionTarget, Assets.Asset[AssetIndex].OffsetLandmarks, &RunningIndex); + + RunningIndex += DeletionTarget.Length; + + ProcessNextLandmarks(N, FirstLandmark, ExistingLandmarkCount, Assets.Asset[AssetIndex].OffsetLandmarks, &RunningIndex, EDIT_DELETION); + DB.Metadata.Buffer.Ptr += sizeof(db_landmark) * ExistingLandmarkCount; + } + CycleFile(&DB.Metadata); +} + +void UpdateLandmarksForNeighbourhood(neighbourhood *N, enum8(edit_types) EditType); + +void +AddLandmarks(neighbourhood *N, enum8(edit_types) EditType) +{ + for(int i = 0; i < Assets.Count; ++i) + { + Assets.Asset[i].Known = FALSE; + } + + for(int StoredAssetIndex = 0; StoredAssetIndex < DB.AssetsHeader.Count; ++StoredAssetIndex) + { + DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr; + int ExistingLandmarkCount = DB.Asset.LandmarkCount; + DB.Metadata.Buffer.Ptr += sizeof(DB.Asset); + void *FirstLandmark; + if(ExistingLandmarkCount > 0) + { + FirstLandmark = DB.Metadata.Buffer.Ptr; + } + int RunningIndex = 0; + + for(int i = 0; i < Assets.Count; ++i) + { + if(!StringsDiffer(DB.Asset.Filename, Assets.Asset[i].Filename) && DB.Asset.Type == Assets.Asset[i].Type) + { + Assets.Asset[i].Known = TRUE; + DB.Asset.LandmarkCount += Assets.Asset[i].PlayerLandmarkCount; + landmark_range ThisTarget; + if(ExistingLandmarkCount > 0) { - char *Ptr = BaseFilename + StringLength(Config.ProjectID); - CopyStringToBuffer(DirectoryPath, "/%s%s", Config.PlayerURLPrefix, Ptr); + ThisTarget = BinarySearchForMetadataLandmark(FirstLandmark, N->ThisIndex, ExistingLandmarkCount); + + if(EditType == EDIT_REINSERTION) { DB.Asset.LandmarkCount -= ThisTarget.Length; } } - else + + fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle); + + if(ExistingLandmarkCount > 0) { - CopyStringToBuffer(DirectoryPath, "/%s", BaseFilename); + ProcessPrevLandmarks(N, FirstLandmark, ExistingLandmarkCount, &ThisTarget, Assets.Asset[i].OffsetLandmarks, &RunningIndex); + } + + for(int j = 0; j < Assets.Asset[i].PlayerLandmarkCount; ++j) + { + db_landmark Landmark; + Landmark.EntryIndex = N->ThisIndex; + Landmark.Position = Assets.Asset[i].PlayerLandmark[j]; + fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.Handle); + } + + if(ExistingLandmarkCount > 0) + { + if(EditType == EDIT_REINSERTION) { RunningIndex += ThisTarget.Length; } + + ProcessNextLandmarks(N, FirstLandmark, ExistingLandmarkCount, Assets.Asset[i].OffsetLandmarks, &RunningIndex, EditType); + } + Assets.Asset[i].OffsetLandmarks = TRUE; + break; + } + } + DB.Metadata.Buffer.Ptr += sizeof(db_landmark) * ExistingLandmarkCount; + } + CycleFile(&DB.Metadata); + + bool NewAsset = FALSE; + for(int i = 0; i < Assets.Count; ++i) + { + if(!Assets.Asset[i].Known && Assets.Asset[i].PlayerLandmarkCount > 0) + { + UpdateAssetInDB(i); + NewAsset = TRUE; + } + } + if(NewAsset) { + UpdateLandmarksForNeighbourhood(N, EDIT_REINSERTION); // NOTE(matt): EDIT_REINSERTION this time because all existing + // assets will have been updated in the first pass + } +} + +void +UpdateLandmarksForNeighbourhood(neighbourhood *N, enum8(edit_types) EditType) +{ + if(!(Config.Mode & MODE_NOREVVEDRESOURCE)) + { + WritePastAssetsHeader(); + switch(EditType) + { + case EDIT_DELETION: DeleteLandmarks(N); break; + case EDIT_ADDITION: case EDIT_REINSERTION: { AddLandmarks(N, EditType); break; } + } + } +} + +void +DeleteLandmarksForSearch(void) +{ + if(!(Config.Mode & MODE_NOREVVEDRESOURCE)) + { + WritePastAssetsHeader(); + for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex) + { + DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr; + DB.Metadata.Buffer.Ptr += sizeof(DB.Asset); + db_landmark *FirstLandmark = (db_landmark *)DB.Metadata.Buffer.Ptr; + int ExistingLandmarkCount = DB.Asset.LandmarkCount; + + landmark_range DeletionTarget = BinarySearchForMetadataLandmark(FirstLandmark, PAGE_TYPE_SEARCH, ExistingLandmarkCount); + DB.Asset.LandmarkCount -= DeletionTarget.Length; + fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle); + + DB.Metadata.Buffer.Ptr += sizeof(DB.Landmark) * DeletionTarget.Length; + fwrite(DB.Metadata.Buffer.Ptr, sizeof(DB.Landmark), ExistingLandmarkCount - DeletionTarget.Length, DB.Metadata.Handle); + + DB.Metadata.Buffer.Ptr += sizeof(DB.Landmark) * ExistingLandmarkCount - DeletionTarget.Length; + } + CycleFile(&DB.Metadata); + } +} + +void +UpdateLandmarksForSearch(void) +{ + if(!(Config.Mode & MODE_NOREVVEDRESOURCE)) + { + for(int i = 0; i < Assets.Count; ++i) + { + Assets.Asset[i].Known = FALSE; + } + + WritePastAssetsHeader(); + for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex) + { + DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr; + int ExistingLandmarkCount = DB.Asset.LandmarkCount; + DB.Metadata.Buffer.Ptr += sizeof(DB.Asset); + void *FirstLandmark = DB.Metadata.Buffer.Ptr; + + for(int i = 0; i < Assets.Count; ++i) + { + if(!StringsDiffer(DB.Asset.Filename, Assets.Asset[i].Filename) && DB.Asset.Type == Assets.Asset[i].Type) + { + Assets.Asset[i].Known = TRUE; + + landmark_range Target = BinarySearchForMetadataLandmark(FirstLandmark, PAGE_TYPE_SEARCH, DB.Asset.LandmarkCount); + + DB.Asset.LandmarkCount += Assets.Asset[i].SearchLandmarkCount - Target.Length; + + fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle); + + for(int j = 0; j < Assets.Asset[i].SearchLandmarkCount; ++j) + { + DB.Landmark.EntryIndex = PAGE_TYPE_SEARCH; + DB.Landmark.Position = Assets.Asset[i].SearchLandmark[j]; + fwrite(&DB.Landmark, sizeof(DB.Landmark), 1, DB.Metadata.Handle); + } + + DB.Metadata.Buffer.Ptr += sizeof(DB.Landmark) * (Target.First + Target.Length); + fwrite(DB.Metadata.Buffer.Ptr, sizeof(DB.Landmark), ExistingLandmarkCount - (Target.First + Target.Length), DB.Metadata.Handle); + DB.Metadata.Buffer.Ptr += sizeof(DB.Landmark) * (ExistingLandmarkCount - (Target.First + Target.Length)); + break; } } - break; + } + CycleFile(&DB.Metadata); + + bool NewAsset = FALSE; + for(int InternalAssetIndex = 0; InternalAssetIndex < Assets.Count; ++InternalAssetIndex) + { + if(!Assets.Asset[InternalAssetIndex].Known && Assets.Asset[InternalAssetIndex].SearchLandmarkCount > 0) + { + NewAsset = TRUE; + UpdateAssetInDB(InternalAssetIndex); + } + } + if(NewAsset) { UpdateLandmarksForSearch(); } } } @@ -5144,19 +6960,20 @@ enum enum { - LINK_PREV, - LINK_NEXT + LINK_FORWARDS, + LINK_BACKWARDS } link_directions; int -InsertNeighbourLink(file_buffer *FromFile, index_metadata *From, index_metadata *To, int LinkDirection, char *ProjectName, bool FromHasOneNeighbour) +InsertNeighbourLink(db_entry *From, int FromIndex, db_entry *To, enum8(link_directions) LinkDirection, bool FromHasOneNeighbour) { - if(ReadFileIntoBuffer(FromFile, 0) == RC_SUCCESS) + file_buffer HTML; + if(ReadPlayerPageIntoBuffer(&HTML, From) == RC_SUCCESS) { - if(!(FromFile->Handle = fopen(FromFile->Path, "w"))) { FreeBuffer(&FromFile->Buffer); return RC_ERROR_FILE; }; + if(!(HTML.Handle = fopen(HTML.Path, "w"))) { FreeBuffer(&HTML.Buffer); return RC_ERROR_FILE; }; buffer Link; - ClaimBuffer(&Link, "Link", 4096); + ClaimBuffer(&Link, "Link", Kilobytes(4)); buffer ToPlayerURL; if(To) @@ -5165,13 +6982,13 @@ InsertNeighbourLink(file_buffer *FromFile, index_metadata *From, index_metadata ConstructPlayerURL(&ToPlayerURL, To->BaseFilename); } + int NewPrevEnd = 0; + int NewNextEnd = 0; switch(LinkDirection) { - case LINK_PREV: + case LINK_BACKWARDS: { - int NewPrevEnd = 0; - int NewNextEnd = 0; - fwrite(FromFile->Buffer.Location, From->LinkOffsets.PrevStart, 1, FromFile->Handle); + fwrite(HTML.Buffer.Location, From->LinkOffsets.PrevStart, 1, HTML.Handle); if(To) { CopyStringToBuffer(&Link, @@ -5182,52 +6999,50 @@ InsertNeighbourLink(file_buffer *FromFile, index_metadata *From, index_metadata else { CopyStringToBuffer(&Link, - " <div class=\"episodeMarker first\"><div>•</div><div>Welcome to <cite>%s</cite></div><div>•</div></div>\n", ProjectName); + " <div class=\"episodeMarker first\"><div>•</div><div>Welcome to <cite>%s</cite></div><div>•</div></div>\n", DB.EntriesHeader.ProjectName); } NewPrevEnd = Link.Ptr - Link.Location; - fwrite(Link.Location, (Link.Ptr - Link.Location), 1, FromFile->Handle); + fwrite(Link.Location, (Link.Ptr - Link.Location), 1, HTML.Handle); if(FromHasOneNeighbour) { - fwrite(FromFile->Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd, From->LinkOffsets.NextStart, 1, FromFile->Handle); + fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd, From->LinkOffsets.NextStart, 1, HTML.Handle); RewindBuffer(&Link); CopyStringToBuffer(&Link, - " <div class=\"episodeMarker last\"><div>•</div><div>You have arrived at the (current) end of <cite>%s</cite></div><div>•</div></div>\n", ProjectName); + " <div class=\"episodeMarker last\"><div>•</div><div>You have arrived at the (current) end of <cite>%s</cite></div><div>•</div></div>\n", DB.EntriesHeader.ProjectName); NewNextEnd = Link.Ptr - Link.Location; - fwrite(Link.Location, NewNextEnd, 1, FromFile->Handle); - fwrite(FromFile->Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd, - FromFile->FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd), + fwrite(Link.Location, NewNextEnd, 1, HTML.Handle); + fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd, + HTML.FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd), 1, - FromFile->Handle); + HTML.Handle); } else { - fwrite(FromFile->Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd, - FromFile->FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd), + fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd, + HTML.FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd), 1, - FromFile->Handle); + HTML.Handle); } From->LinkOffsets.PrevEnd = NewPrevEnd; if(FromHasOneNeighbour) { From->LinkOffsets.NextEnd = NewNextEnd; } } break; - case LINK_NEXT: + case LINK_FORWARDS: { - int NewPrevEnd = 0; - int NewNextEnd = 0; if(FromHasOneNeighbour) { - fwrite(FromFile->Buffer.Location, From->LinkOffsets.PrevStart, 1, FromFile->Handle); + fwrite(HTML.Buffer.Location, From->LinkOffsets.PrevStart, 1, HTML.Handle); CopyStringToBuffer(&Link, - " <div class=\"episodeMarker first\"><div>•</div><div>Welcome to <cite>%s</cite></div><div>•</div></div>\n", ProjectName); + " <div class=\"episodeMarker first\"><div>•</div><div>Welcome to <cite>%s</cite></div><div>•</div></div>\n", DB.EntriesHeader.ProjectName); NewPrevEnd = Link.Ptr - Link.Location; - fwrite(Link.Location, NewPrevEnd, 1, FromFile->Handle); + fwrite(Link.Location, NewPrevEnd, 1, HTML.Handle); RewindBuffer(&Link); - fwrite(FromFile->Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd, - From->LinkOffsets.NextStart, 1, FromFile->Handle); + fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd, + From->LinkOffsets.NextStart, 1, HTML.Handle); } else { - fwrite(FromFile->Buffer.Location, From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart, 1, FromFile->Handle); + fwrite(HTML.Buffer.Location, From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart, 1, HTML.Handle); } if(To) @@ -5240,15 +7055,15 @@ InsertNeighbourLink(file_buffer *FromFile, index_metadata *From, index_metadata else { CopyStringToBuffer(&Link, - " <div class=\"episodeMarker last\"><div>•</div><div>You have arrived at the (current) end of <cite>%s</cite></div><div>•</div></div>\n", ProjectName); + " <div class=\"episodeMarker last\"><div>•</div><div>You have arrived at the (current) end of <cite>%s</cite></div><div>•</div></div>\n", DB.EntriesHeader.ProjectName); } NewNextEnd = Link.Ptr - Link.Location; - fwrite(Link.Location, (Link.Ptr - Link.Location), 1, FromFile->Handle); + fwrite(Link.Location, (Link.Ptr - Link.Location), 1, HTML.Handle); - fwrite(FromFile->Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd, - FromFile->FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd), + fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd, + HTML.FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd), 1, - FromFile->Handle); + HTML.Handle); if(FromHasOneNeighbour) { From->LinkOffsets.PrevEnd = NewPrevEnd; } From->LinkOffsets.NextEnd = NewNextEnd; @@ -5257,8 +7072,9 @@ InsertNeighbourLink(file_buffer *FromFile, index_metadata *From, index_metadata if(To) { DeclaimBuffer(&ToPlayerURL); } DeclaimBuffer(&Link); - fclose(FromFile->Handle); - FreeBuffer(&FromFile->Buffer); + fclose(HTML.Handle); + FreeBuffer(&HTML.Buffer); + SnipeEntryIntoMetadataBuffer(From, FromIndex); return RC_SUCCESS; } else @@ -5268,131 +7084,253 @@ InsertNeighbourLink(file_buffer *FromFile, index_metadata *From, index_metadata } int -DeleteNeighbourLinks(file_buffer *File, index_metadata *Metadata) +DeleteNeighbourLinks(neighbourhood *N) { - if(ReadFileIntoBuffer(File, 0) == RC_SUCCESS) + db_entry *Entry; + int EntryIndex; + if(N->PrevIndex >= 0) { - if(!(File->Handle = fopen(File->Path, "w"))) { FreeBuffer(&File->Buffer); return RC_ERROR_FILE; }; + Entry = &N->Prev; + EntryIndex = N->PrevIndex; - fwrite(File->Buffer.Location, Metadata->LinkOffsets.PrevStart, 1, File->Handle); - fwrite(File->Buffer.Location + Metadata->LinkOffsets.PrevStart + Metadata->LinkOffsets.PrevEnd, Metadata->LinkOffsets.NextStart, 1, File->Handle); - fwrite(File->Buffer.Location + Metadata->LinkOffsets.PrevStart + Metadata->LinkOffsets.PrevEnd + Metadata->LinkOffsets.NextStart + Metadata->LinkOffsets.NextEnd, - File->FileSize - (Metadata->LinkOffsets.PrevStart + Metadata->LinkOffsets.PrevEnd + Metadata->LinkOffsets.NextStart + Metadata->LinkOffsets.NextEnd), - 1, - File->Handle); - fclose(File->Handle); - Metadata->LinkOffsets.PrevEnd = 0; - Metadata->LinkOffsets.NextEnd = 0; - FreeBuffer(&File->Buffer); - return RC_SUCCESS; - } - else { return RC_ERROR_FILE; } -} - -int -LinkNeighbours(index *Index, neighbourhood *N, char *BaseFilename, int LinkType) -{ - if(N->PrevIndex == -1 && N->NextIndex == -1) - { - buffer LonePlayerPagePath; - ClaimBuffer(&LonePlayerPagePath, "LonePlayerPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10); - ConstructDirectoryPath(&LonePlayerPagePath, PAGE_PLAYER, Config.PlayerLocation, N->This.BaseFilename); - CopyStringToBuffer(&LonePlayerPagePath, "/index.html"); - - file_buffer LonePlayerPage; - CopyStringNoFormat(LonePlayerPage.Path, sizeof(LonePlayerPage.Path), LonePlayerPagePath.Location); - - DeleteNeighbourLinks(&LonePlayerPage, &N->This); - DeclaimBuffer(&LonePlayerPagePath); + N->PreLinkPrevOffsetTotal = N->Prev.LinkOffsets.PrevEnd + + N->Prev.LinkOffsets.NextStart + + N->Prev.LinkOffsets.NextEnd; } else { + Entry = &N->Next; + EntryIndex = N->NextIndex; + + N->PreLinkNextOffsetTotal = N->Next.LinkOffsets.PrevEnd + + N->Next.LinkOffsets.NextStart + + N->Next.LinkOffsets.NextEnd; + } + + file_buffer HTML; + if(ReadPlayerPageIntoBuffer(&HTML, Entry) == RC_SUCCESS) + { + if(!(HTML.Handle = fopen(HTML.Path, "w"))) { FreeBuffer(&HTML.Buffer); return RC_ERROR_FILE; }; + + fwrite(HTML.Buffer.Location, Entry->LinkOffsets.PrevStart, 1, HTML.Handle); + fwrite(HTML.Buffer.Location + Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd, Entry->LinkOffsets.NextStart, 1, HTML.Handle); + fwrite(HTML.Buffer.Location + Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd + Entry->LinkOffsets.NextStart + Entry->LinkOffsets.NextEnd, + HTML.FileSize - (Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd + Entry->LinkOffsets.NextStart + Entry->LinkOffsets.NextEnd), + 1, + HTML.Handle); + fclose(HTML.Handle); + Entry->LinkOffsets.PrevEnd = 0; + Entry->LinkOffsets.NextEnd = 0; if(N->PrevIndex >= 0) { - buffer PreviousPlayerPagePath; - ClaimBuffer(&PreviousPlayerPagePath, "PreviousPlayerPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10); - ConstructDirectoryPath(&PreviousPlayerPagePath, PAGE_PLAYER, Config.PlayerLocation, N->Prev.BaseFilename); - CopyStringToBuffer(&PreviousPlayerPagePath, "/index.html"); - - file_buffer PreviousPlayerPage; - CopyStringNoFormat(PreviousPlayerPage.Path, sizeof(PreviousPlayerPage.Path), PreviousPlayerPagePath.Location); - - switch(LinkType) - { - case LINK_EXCLUDE: - { - buffer ThisPlayerPagePath; - ClaimBuffer(&ThisPlayerPagePath, "ThisPlayerPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10); - ConstructDirectoryPath(&ThisPlayerPagePath, PAGE_PLAYER, Config.PlayerLocation, N->This.BaseFilename); - CopyStringToBuffer(&ThisPlayerPagePath, "/index.html"); - - file_buffer ThisPlayerPage; - CopyStringNoFormat(ThisPlayerPage.Path, sizeof(ThisPlayerPage.Path), ThisPlayerPagePath.Location); - - InsertNeighbourLink(&ThisPlayerPage, &N->This, &N->Prev, LINK_PREV, Index->Header.ProjectName, N->NextIndex == -1 ? TRUE : FALSE); - - DeclaimBuffer(&ThisPlayerPagePath); - } - case LINK_INCLUDE: - { - InsertNeighbourLink(&PreviousPlayerPage, &N->Prev, &N->This, LINK_NEXT, Index->Header.ProjectName, N->PrevIsFirst); - *(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * N->PrevIndex) = N->Prev; - } - } - - DeclaimBuffer(&PreviousPlayerPagePath); + N->PrevOffsetModifier = N->Prev.LinkOffsets.PrevEnd + + N->Prev.LinkOffsets.NextStart + + N->Prev.LinkOffsets.NextEnd + - N->PreLinkPrevOffsetTotal; } - - if(N->NextIndex >= 0) + else { - buffer NextPlayerPagePath; - ClaimBuffer(&NextPlayerPagePath, "NextPlayerPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10); - ConstructDirectoryPath(&NextPlayerPagePath, PAGE_PLAYER, Config.PlayerLocation, N->Next.BaseFilename); - CopyStringToBuffer(&NextPlayerPagePath, "/index.html"); - - file_buffer NextPlayerPage; - CopyStringNoFormat(NextPlayerPage.Path, sizeof(NextPlayerPage.Path), NextPlayerPagePath.Location); - - switch(LinkType) - { - case LINK_EXCLUDE: - { - buffer ThisPlayerPagePath; - ClaimBuffer(&ThisPlayerPagePath, "ThisPlayerPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10); - ConstructDirectoryPath(&ThisPlayerPagePath, PAGE_PLAYER, Config.PlayerLocation, N->This.BaseFilename); - CopyStringToBuffer(&ThisPlayerPagePath, "/index.html"); - - file_buffer ThisPlayerPage; - CopyStringNoFormat(ThisPlayerPage.Path, sizeof(ThisPlayerPage.Path), ThisPlayerPagePath.Location); - - InsertNeighbourLink(&ThisPlayerPage, &N->This, &N->Next, LINK_NEXT, Index->Header.ProjectName, N->PrevIndex == -1 ? TRUE : FALSE); - - DeclaimBuffer(&ThisPlayerPagePath); - } - case LINK_INCLUDE: - { - InsertNeighbourLink(&NextPlayerPage, &N->Next, &N->This, LINK_PREV, Index->Header.ProjectName, N->NextIsFinal); - *(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * N->NextIndex) = N->Next; - } - } - - DeclaimBuffer(&NextPlayerPagePath); + N->NextOffsetModifier = N->Next.LinkOffsets.PrevEnd + + N->Next.LinkOffsets.NextStart + + N->Next.LinkOffsets.NextEnd + - N->PreLinkNextOffsetTotal; } + FreeBuffer(&HTML.Buffer); + SnipeEntryIntoMetadataBuffer(Entry, EntryIndex); } + return RC_SUCCESS; } void -DeleteIndexPageFromFilesystem() // NOTE(matt): Do we need to handle relocating, like the PlayerPage function? +LinkToNewEntry(neighbourhood *N) { - buffer IndexDirectory; - ClaimBuffer(&IndexDirectory, "IndexDirectory", 1024); - ConstructDirectoryPath(&IndexDirectory, PAGE_INDEX, Config.IndexLocation, ""); - char IndexPagePath[1024]; - CopyString(IndexPagePath, sizeof(IndexPagePath), "%s/index.html", IndexDirectory.Location); - remove(IndexPagePath); - remove(IndexDirectory.Location); - DeclaimBuffer(&IndexDirectory); + N->PreLinkThisOffsetTotal = N->This.LinkOffsets.PrevEnd + + N->This.LinkOffsets.NextStart + + N->This.LinkOffsets.NextEnd; + + if(N->PrevIndex >= 0) + { + N->PreLinkPrevOffsetTotal = N->Prev.LinkOffsets.PrevEnd + + N->Prev.LinkOffsets.NextStart + + N->Prev.LinkOffsets.NextEnd; + + InsertNeighbourLink(&N->Prev, N->PrevIndex, &N->This, LINK_FORWARDS, N->FormerIsFirst); + + N->PrevOffsetModifier = N->Prev.LinkOffsets.PrevEnd + + N->Prev.LinkOffsets.NextStart + + N->Prev.LinkOffsets.NextEnd + - N->PreLinkPrevOffsetTotal; + } + + if(N->NextIndex >= 0) + { + N->PreLinkNextOffsetTotal = N->Next.LinkOffsets.PrevEnd + + N->Next.LinkOffsets.NextStart + + N->Next.LinkOffsets.NextEnd; + + InsertNeighbourLink(&N->Next, N->NextIndex, &N->This, LINK_BACKWARDS, N->LatterIsFinal); + + N->NextOffsetModifier = N->Next.LinkOffsets.PrevEnd + + N->Next.LinkOffsets.NextStart + + N->Next.LinkOffsets.NextEnd + - N->PreLinkNextOffsetTotal; + } +} + +void +MarkNextAsFirst(neighbourhood *N) +{ + file_buffer HTML; + ReadPlayerPageIntoBuffer(&HTML, &N->Next); + + buffer Link; + ClaimBuffer(&Link, "Link", Kilobytes(4)); + + HTML.Handle = fopen(HTML.Path, "w"); + + fwrite(HTML.Buffer.Location, N->Next.LinkOffsets.PrevStart, 1, HTML.Handle); + + CopyStringToBuffer(&Link, + " <div class=\"episodeMarker first\"><div>•</div><div>Welcome to <cite>%s</cite></div><div>•</div></div>\n", DB.EntriesHeader.ProjectName); + + fwrite(Link.Location, (Link.Ptr - Link.Location), 1, HTML.Handle); + + fwrite(HTML.Buffer.Location + N->Next.LinkOffsets.PrevStart + N->Next.LinkOffsets.PrevEnd, + HTML.FileSize - (N->Next.LinkOffsets.PrevStart + N->Next.LinkOffsets.PrevEnd), + 1, + HTML.Handle); + + N->Next.LinkOffsets.PrevEnd = Link.Ptr - Link.Location; + + DeclaimBuffer(&Link); + fclose(HTML.Handle); + FreeBuffer(&HTML.Buffer); + + SnipeEntryIntoMetadataBuffer(&N->Next, N->NextIndex); +} + +void +MarkPrevAsFinal(neighbourhood *N) +{ + file_buffer File; + ReadPlayerPageIntoBuffer(&File, &N->Prev); + + File.Handle = fopen(File.Path, "w"); + buffer Link; + ClaimBuffer(&Link, "Link", Kilobytes(4)); + + + fwrite(File.Buffer.Location, N->Prev.LinkOffsets.PrevStart + N->Prev.LinkOffsets.PrevEnd + N->Prev.LinkOffsets.NextStart, 1, File.Handle); + + CopyStringToBuffer(&Link, + " <div class=\"episodeMarker last\"><div>•</div><div>You have arrived at the (current) end of <cite>%s</cite></div><div>•</div></div>\n", DB.EntriesHeader.ProjectName); + + fwrite(Link.Location, (Link.Ptr - Link.Location), 1, File.Handle); + + fwrite(File.Buffer.Location + N->Prev.LinkOffsets.PrevStart + N->Prev.LinkOffsets.PrevEnd + N->Prev.LinkOffsets.NextStart + N->Prev.LinkOffsets.NextEnd, + File.FileSize - (N->Prev.LinkOffsets.PrevStart + N->Prev.LinkOffsets.PrevEnd + N->Prev.LinkOffsets.NextStart + N->Prev.LinkOffsets.NextEnd), + 1, + File.Handle); + + N->Prev.LinkOffsets.NextEnd = Link.Ptr - Link.Location; + + DeclaimBuffer(&Link); + fclose(File.Handle); + FreeBuffer(&File.Buffer); + + SnipeEntryIntoMetadataBuffer(&N->Prev, N->PrevIndex); +} + +void +LinkOverDeletedEntry(neighbourhood *N) +{ + if(DB.EntriesHeader.Count == 1) + { + DeleteNeighbourLinks(N); + } + else + { + if(N->DeletedEntryWasFirst) + { + N->PreLinkNextOffsetTotal = N->Next.LinkOffsets.PrevEnd + + N->Next.LinkOffsets.NextStart + + N->Next.LinkOffsets.NextEnd; + + MarkNextAsFirst(N); + + N->NextOffsetModifier = N->Next.LinkOffsets.PrevEnd + + N->Next.LinkOffsets.NextStart + + N->Next.LinkOffsets.NextEnd + - N->PreLinkNextOffsetTotal; + return; + } + else if(N->DeletedEntryWasFinal) + { + N->PreLinkPrevOffsetTotal = N->Prev.LinkOffsets.PrevEnd + + N->Prev.LinkOffsets.NextStart + + N->Prev.LinkOffsets.NextEnd; + + MarkPrevAsFinal(N); + + N->PrevOffsetModifier = N->Prev.LinkOffsets.PrevEnd + + N->Prev.LinkOffsets.NextStart + + N->Prev.LinkOffsets.NextEnd + - N->PreLinkPrevOffsetTotal; + return; + } + else + { + // Assert(N->PrevIndex >= 0 && N->NextIndex >= 0) + N->PreLinkPrevOffsetTotal = N->Prev.LinkOffsets.PrevEnd + + N->Prev.LinkOffsets.NextStart + + N->Prev.LinkOffsets.NextEnd; + + + N->PreLinkNextOffsetTotal = N->Next.LinkOffsets.PrevEnd + + N->Next.LinkOffsets.NextStart + + N->Next.LinkOffsets.NextEnd; + } + + InsertNeighbourLink(&N->Prev, N->PrevIndex, &N->Next, LINK_FORWARDS, N->FormerIsFirst); + InsertNeighbourLink(&N->Next, N->NextIndex, &N->Prev, LINK_BACKWARDS, N->LatterIsFinal); + + N->PrevOffsetModifier = N->Prev.LinkOffsets.PrevEnd + + N->Prev.LinkOffsets.NextStart + + N->Prev.LinkOffsets.NextEnd + - N->PreLinkPrevOffsetTotal; + + N->NextOffsetModifier = N->Next.LinkOffsets.PrevEnd + + N->Next.LinkOffsets.NextStart + + N->Next.LinkOffsets.NextEnd + - N->PreLinkNextOffsetTotal; + } +} + +void +LinkNeighbours(neighbourhood *N, enum8(link_types) LinkType) +{ + if(LinkType == LINK_INCLUDE) + { + LinkToNewEntry(N); + } + else + { + LinkOverDeletedEntry(N); + } +} + +void +DeleteSearchPageFromFilesystem() // NOTE(matt): Do we need to handle relocating, like the PlayerPage function? +{ + buffer SearchDirectory; + ClaimBuffer(&SearchDirectory, "SearchDirectory", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + 10); + ConstructDirectoryPath(&SearchDirectory, PAGE_SEARCH, Config.SearchLocation, ""); + char SearchPagePath[1024]; + CopyString(SearchPagePath, sizeof(SearchPagePath), "%s/index.html", SearchDirectory.Location); + remove(SearchPagePath); + remove(SearchDirectory.Location); + DeclaimBuffer(&SearchDirectory); } int @@ -5400,7 +7338,7 @@ DeletePlayerPageFromFilesystem(char *BaseFilename, char *PlayerLocation, bool Re { // NOTE(matt): Once we have the notion of an output filename format, we'll need to use that here buffer OutputDirectoryPath; - ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", 1024); + ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10); ConstructDirectoryPath(&OutputDirectoryPath, PAGE_PLAYER, PlayerLocation, BaseFilename); DIR *PlayerDir; @@ -5419,14 +7357,14 @@ DeletePlayerPageFromFilesystem(char *BaseFilename, char *PlayerLocation, bool Re if((remove(OutputDirectoryPath.Location) == -1)) { LogError(LOG_NOTICE, "Mostly deleted %s. Unable to remove directory %s: %s", BaseFilename, OutputDirectoryPath.Location, strerror(errno)); - fprintf(stderr, "\e[1;30mMostly deleted\e[0m %s. \e[1;31mUnable to remove directory\e[0m %s: %s", BaseFilename, OutputDirectoryPath.Location, strerror(errno)); + fprintf(stderr, "%sMostly deleted%s %s. %sUnable to remove directory%s %s: %s", ColourStrings[EditTypes[EDIT_DELETION].Colour], ColourStrings[CS_END], BaseFilename, ColourStrings[CS_ERROR], ColourStrings[CS_END], OutputDirectoryPath.Location, strerror(errno)); } else { if(!Relocating) { LogError(LOG_INFORMATIONAL, "Deleted %s", BaseFilename); - fprintf(stderr, "\e[1;30mDeleted\e[0m %s\n", BaseFilename); + fprintf(stderr, "%sDeleted%s %s\n", ColourStrings[EditTypes[EDIT_DELETION].Colour], ColourStrings[CS_END], BaseFilename); } } @@ -5436,56 +7374,51 @@ DeletePlayerPageFromFilesystem(char *BaseFilename, char *PlayerLocation, bool Re } int -DeleteFromIndex(index *Index, neighbourhood *N, char *BaseFilename) +DeleteFromDB(neighbourhood *N, char *BaseFilename) { // TODO(matt): LogError() - Index->Header = *(index_header *)Index->Metadata.Buffer.Location; - Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + sizeof(Index->Header); + DB.Header = *(db_header *)DB.Metadata.Buffer.Location; + DB.EntriesHeader = *(db_header_entries *)(DB.Metadata.Buffer.Location + sizeof(DB.Header)); - index_metadata *Entry = { 0 }; - int EntryIndex = BinarySearchForMetadataEntry(Index, &Entry, BaseFilename); + db_entry *Entry = { 0 }; + int EntryIndex = BinarySearchForMetadataEntry(&Entry, BaseFilename); if(Entry) { - int DeleteFileFrom = AccumulateIndexEntryInsertionOffset(Index, EntryIndex); + int DeleteFileFrom = AccumulateDBEntryInsertionOffset(EntryIndex); int DeleteFileTo = DeleteFileFrom + Entry->Size; - bool ThisIsPrev = FALSE; N->ThisIndex = EntryIndex; - GetNeighbourhood(Index, N, EDIT_DELETION, &ThisIsPrev); - --Index->Header.EntryCount; + GetNeighbourhood(N, EDIT_DELETION); + --DB.EntriesHeader.Count; - if(Index->Header.EntryCount == 0) + if(DB.EntriesHeader.Count == 0) { - DeleteIndexPageFromFilesystem(); + // TODO(matt): Handle this differently, allowing 0 entries but > 0 assets? + DeleteSearchPageFromFilesystem(); - remove(Index->Metadata.Path); - Index->Metadata.FileSize = 0; - FreeBuffer(&Index->Metadata.Buffer); + remove(DB.Metadata.Path); + DB.Metadata.FileSize = 0; + FreeBuffer(&DB.Metadata.Buffer); - remove(Index->File.Path); - Index->File.FileSize = 0; - FreeBuffer(&Index->File.Buffer); + remove(DB.File.Path); + DB.File.FileSize = 0; + FreeBuffer(&DB.File.Buffer); } else { - if(!(Index->Metadata.Handle = fopen(Index->Metadata.Path, "w"))) { FreeBuffer(&Index->Metadata.Buffer); return RC_ERROR_FILE; } - if(!(Index->File.Handle = fopen(Index->File.Path, "w"))) { FreeBuffer(&Index->File.Buffer); return RC_ERROR_FILE; } + if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; } + if(!(DB.File.Handle = fopen(DB.File.Path, "w"))) { FreeBuffer(&DB.File.Buffer); return RC_ERROR_FILE; } - fwrite(&Index->Header, sizeof(Index->Header), 1, Index->Metadata.Handle); - fwrite(Index->Metadata.Buffer.Location + sizeof(index_header), sizeof(index_metadata) * EntryIndex, 1, Index->Metadata.Handle); - fwrite(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * (EntryIndex + 1), - Index->Metadata.FileSize - sizeof(index_header) - sizeof(index_metadata) * (EntryIndex + 1), - 1, Index->Metadata.Handle); - fclose(Index->Metadata.Handle); + fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle); + fwrite(&DB.EntriesHeader, sizeof(DB.EntriesHeader), 1, DB.Metadata.Handle); + fwrite(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader), sizeof(DB.Entry) * EntryIndex, 1, DB.Metadata.Handle); + fwrite(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * (EntryIndex + 1), + DB.Metadata.FileSize - sizeof(DB.Header) - sizeof(DB.EntriesHeader) - sizeof(DB.Entry) * (EntryIndex + 1), + 1, DB.Metadata.Handle); + CycleFile(&DB.Metadata); - FreeBuffer(&Index->Metadata.Buffer); - ReadFileIntoBuffer(&Index->Metadata, 0); - - fwrite(Index->File.Buffer.Location, DeleteFileFrom, 1, Index->File.Handle); - fwrite(Index->File.Buffer.Location + DeleteFileTo, Index->File.FileSize - DeleteFileTo, 1, Index->File.Handle); - fclose(Index->File.Handle); - - FreeBuffer(&Index->File.Buffer); - ReadFileIntoBuffer(&Index->File, 0); + fwrite(DB.File.Buffer.Location, DeleteFileFrom, 1, DB.File.Handle); + fwrite(DB.File.Buffer.Location + DeleteFileTo, DB.File.FileSize - DeleteFileTo, 1, DB.File.Handle); + CycleFile(&DB.File); } } @@ -5493,185 +7426,193 @@ DeleteFromIndex(index *Index, neighbourhood *N, char *BaseFilename) } int -IndexToBuffer(index *Index, buffers *CollationBuffers) // NOTE(matt): This guy malloc's CollationBuffers->Index +SearchToBuffer(buffers *CollationBuffers) // NOTE(matt): This guy malloc's CollationBuffers->Search { - if(Index->Metadata.FileSize > 0) + if(DB.Metadata.Buffer.Location) { - Index->Header = *(index_header*)Index->Metadata.Buffer.Location; - - int ProjectIndex; - for(ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex) + DB.Header = *(db_header *)DB.Metadata.Buffer.Location; + DB.EntriesHeader = *(db_header_entries *)(DB.Metadata.Buffer.Location + sizeof(DB.Header)); + if(DB.EntriesHeader.Count > 0) { - if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID)) + RewindBuffer(&CollationBuffers->IncludesSearch); + + buffer URL; + ConstructResolvedAssetURL(&URL, ASSET_CSS_CINERA, PAGE_SEARCH); + CopyStringToBuffer(&CollationBuffers->IncludesSearch, + "<meta charset=\"UTF-8\">\n" + " <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n" + "\n" + " <link rel=\"stylesheet\" type=\"text/css\" href=\"%s", + CINERA_APP_VERSION.Major, + CINERA_APP_VERSION.Minor, + CINERA_APP_VERSION.Patch, + URL.Location); + DeclaimBuffer(&URL); + PushAssetLandmark(&CollationBuffers->IncludesSearch, ASSET_CSS_CINERA, PAGE_SEARCH); + + ConstructResolvedAssetURL(&URL, ASSET_CSS_THEME, PAGE_SEARCH); + CopyStringToBuffer(&CollationBuffers->IncludesSearch, + "\">\n" + " <link rel=\"stylesheet\" type=\"text/css\" href=\"%s", + URL.Location); + DeclaimBuffer(&URL); + PushAssetLandmark(&CollationBuffers->IncludesSearch, ASSET_CSS_THEME, PAGE_SEARCH); + + ConstructResolvedAssetURL(&URL, ASSET_CSS_TOPICS, PAGE_SEARCH); + CopyStringToBuffer(&CollationBuffers->IncludesSearch, + "\">\n" + " <link rel=\"stylesheet\" type=\"text/css\" href=\"%s", + URL.Location); + DeclaimBuffer(&URL); + PushAssetLandmark(&CollationBuffers->IncludesSearch, ASSET_CSS_TOPICS, PAGE_SEARCH); + + CopyStringToBuffer(&CollationBuffers->IncludesSearch, + "\">\n"); + + int ProjectIndex; + for(ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex) { - break; - } - } - - char *Theme = StringsDiffer(Config.Theme, "") ? Config.Theme : Config.ProjectID; - int Allowance = StringLength(Theme) * 2 + StringLength(Config.ProjectID) + StringLength(Config.BaseURL) + StringLength(Config.PlayerLocation); - char queryContainer[678 + Allowance]; // NOTE(matt): Update the size if changing the string - CopyString(queryContainer, sizeof(queryContainer), - "<div class=\"cineraQueryContainer %s\">\n" - " <label for=\"query\">Query:</label>\n" - " <div class=\"inputContainer\">\n" - " <input type=\"text\" autocomplete=\"off\" id=\"query\">\n" - " <div class=\"spinner\">\n" - " Downloading data...\n" - " </div>\n" - " </div>\n" - " </div>\n" - " <div id=\"cineraResultsSummary\">Found: 0 episodes, 0 markers, 0h 0m 0s total.</div>\n" - " <div id=\"cineraResults\" data-single=\"%d\"></div>\n" - "\n" - " <div id=\"cineraIndex\" class=\"%s\" data-project=\"%s\" data-baseURL=\"%s\" data-playerLocation=\"%s\">\n" - " <div id=\"cineraIndexSort\">Sort: Old to New ⏶</div>\n" - " <div id=\"cineraIndexEntries\">\n", - Theme, - Config.Mode & MODE_SINGLETAB ? 1 : 0, - Theme, - Config.ProjectID, - Config.BaseURL, - Config.PlayerLocation); - - buffer URLPrefix; - ClaimBuffer(&URLPrefix, "URLPrefix", 1024); - ConstructURLPrefix(&URLPrefix, INCLUDE_JS, PAGE_INDEX); - char Script[107 + StringLength(URLPrefix.Location)]; // NOTE(matt): Update the size if changing the string - CopyString(Script, sizeof(Script), - " </div>\n" - " </div>\n" - " <script type=\"text/javascript\" src=\"%scinera_search.js\"></script>\n", - URLPrefix.Location); - DeclaimBuffer(&URLPrefix); - - buffer PlayerURL; - ClaimBuffer(&PlayerURL, "PlayerURL", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH); - ConstructPlayerURL(&PlayerURL, ""); - char *ProjectUnit = 0; - if(StringsDiffer(ProjectInfo[ProjectIndex].Unit, "")) - { - ProjectUnit = ProjectInfo[ProjectIndex].Unit; - } - char Number[16]; - index_metadata *This; - char Text[(ProjectUnit ? StringLength(ProjectUnit) : 0) + sizeof(Number) + sizeof(This->Title) + 3]; - - int EntryLength = StringLength(PlayerURL.Location) + sizeof(Text) + 82; - CollationBuffers->Index.Size = StringLength(queryContainer) + (Index->Header.EntryCount * EntryLength) + StringLength(Script); - - if(!(CollationBuffers->Index.Location = malloc(CollationBuffers->Index.Size))) { return RC_ERROR_MEMORY; } - - CollationBuffers->Index.ID = "Index"; - CollationBuffers->Index.Ptr = CollationBuffers->Index.Location; - - CopyStringToBuffer(&CollationBuffers->Index, "%s", queryContainer); - - int ProjectIDLength = StringLength(Config.ProjectID); - bool IndexRequired = FALSE; - - Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + sizeof(Index->Header); - for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex, Index->Metadata.Buffer.Ptr += sizeof(index_metadata)) - { - This = (index_metadata *)Index->Metadata.Buffer.Ptr; - if(This->Size > 0) - { - IndexRequired = TRUE; - CopyString(Number, sizeof(Number), "%s", This->BaseFilename + ProjectIDLength); - if(ProjectInfo[ProjectIndex].NumberingScheme == NS_LINEAR) + if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID)) { - for(int i = 0; Number[i]; ++i) + break; + } + } + + char *Theme = StringsDiffer(Config.Theme, "") ? Config.Theme : Config.ProjectID; + int Allowance = StringLength(Theme) * 2 + StringLength(Config.ProjectID) + StringLength(Config.BaseURL) + StringLength(Config.PlayerLocation); + char queryContainer[678 + Allowance]; // NOTE(matt): Update the size if changing the string + CopyString(queryContainer, sizeof(queryContainer), + "<div class=\"cineraQueryContainer %s\">\n" + " <label for=\"query\">Query:</label>\n" + " <div class=\"inputContainer\">\n" + " <input type=\"text\" autocomplete=\"off\" id=\"query\">\n" + " <div class=\"spinner\">\n" + " Downloading data...\n" + " </div>\n" + " </div>\n" + " </div>\n" + " <div id=\"cineraResultsSummary\">Found: 0 episodes, 0 markers, 0h 0m 0s total.</div>\n" + " <div id=\"cineraResults\" data-single=\"%d\"></div>\n" + "\n" + " <div id=\"cineraIndex\" class=\"%s\" data-project=\"%s\" data-baseURL=\"%s\" data-playerLocation=\"%s\">\n" + " <div id=\"cineraIndexSort\">Sort: Old to New ⏶</div>\n" + " <div id=\"cineraIndexEntries\">\n", + Theme, + Config.Mode & MODE_SINGLETAB ? 1 : 0, + Theme, + Config.ProjectID, + Config.BaseURL, + Config.PlayerLocation); + + ConstructResolvedAssetURL(&URL, ASSET_JS_SEARCH, PAGE_SEARCH); + buffer Script; + ClaimBuffer(&Script, "Script", (117 + URL.Ptr - URL.Location) * 2); // NOTE(matt): Update the size if changing the string + CopyStringToBuffer(&Script, + " </div>\n" + " </div>\n" + " <script type=\"text/javascript\" src=\"%s", URL.Location); + // NOTE(matt): DeclaimBuffer(&URL) happens later, after Script is declaimed because Script was only claimed here + PushAssetLandmark(&Script, ASSET_JS_SEARCH, PAGE_SEARCH); + + CopyStringToBuffer(&Script, + "\"></script>"); + + buffer PlayerURL; + ClaimBuffer(&PlayerURL, "PlayerURL", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH); + ConstructPlayerURL(&PlayerURL, ""); + char *ProjectUnit = 0; + if(StringsDiffer(ProjectInfo[ProjectIndex].Unit, "")) + { + ProjectUnit = ProjectInfo[ProjectIndex].Unit; + } + char Number[16]; + db_entry *This; + char Text[(ProjectUnit ? StringLength(ProjectUnit) : 0) + sizeof(Number) + sizeof(This->Title) + 3]; + + int EntryLength = StringLength(PlayerURL.Location) + sizeof(Text) + 82; + CollationBuffers->Search.Size = StringLength(queryContainer) + (DB.EntriesHeader.Count * EntryLength) + Script.Ptr - Script.Location; + + if(!(CollationBuffers->Search.Location = malloc(CollationBuffers->Search.Size))) { return RC_ERROR_MEMORY; } + + CollationBuffers->Search.ID = "Search"; + CollationBuffers->Search.Ptr = CollationBuffers->Search.Location; + + CopyStringToBuffer(&CollationBuffers->Search, "%s", queryContainer); + + int ProjectIDLength = StringLength(Config.ProjectID); + bool SearchRequired = FALSE; + + DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader); + for(int EntryIndex = 0; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex, DB.Metadata.Buffer.Ptr += sizeof(DB.Entry)) + { + This = (db_entry *)DB.Metadata.Buffer.Ptr; + if(This->Size > 0) + { + SearchRequired = TRUE; + CopyString(Number, sizeof(Number), "%s", This->BaseFilename + ProjectIDLength); + if(ProjectInfo[ProjectIndex].NumberingScheme == NS_LINEAR) { - if(Number[i] == '_') + for(int i = 0; Number[i]; ++i) { - Number[i] = '.'; + if(Number[i] == '_') + { + Number[i] = '.'; + } } } - } - ConstructPlayerURL(&PlayerURL, This->BaseFilename); + ConstructPlayerURL(&PlayerURL, This->BaseFilename); - if(ProjectUnit) - { - CopyStringToBuffer(&CollationBuffers->Index, - " <div>\n" - " <a href=\"%s\">", PlayerURL.Location); + if(ProjectUnit) + { + CopyStringToBuffer(&CollationBuffers->Search, + " <div>\n" + " <a href=\"%s\">", PlayerURL.Location); - CopyString(Text, sizeof(Text), "%s %s: %s", - ProjectUnit, // TODO(matt): Do we need to special-case the various numbering schemes? - Number, - This->Title); + CopyString(Text, sizeof(Text), "%s %s: %s", + ProjectUnit, // TODO(matt): Do we need to special-case the various numbering schemes? + Number, + This->Title); - CopyStringToBufferHTMLSafe(&CollationBuffers->Index, Text); + CopyStringToBufferHTMLSafe(&CollationBuffers->Search, Text); - CopyStringToBuffer(&CollationBuffers->Index, - "</a>\n" - " </div>\n"); - } - else - { - CopyStringToBuffer(&CollationBuffers->Index, - " <div>\n" - " <a href=\"%s\">", PlayerURL.Location); + CopyStringToBuffer(&CollationBuffers->Search, + "</a>\n" + " </div>\n"); + } + else + { + CopyStringToBuffer(&CollationBuffers->Search, + " <div>\n" + " <a href=\"%s\">", PlayerURL.Location); - CopyStringToBufferHTMLSafe(&CollationBuffers->Index, This->Title); - CopyStringToBuffer(&CollationBuffers->Index, - "</a>\n" - " </div>\n"); + CopyStringToBufferHTMLSafe(&CollationBuffers->Search, This->Title); + CopyStringToBuffer(&CollationBuffers->Search, + "</a>\n" + " </div>\n"); + } } } - } - DeclaimBuffer(&PlayerURL); + OffsetLandmarks(&CollationBuffers->Search, ASSET_JS_SEARCH, PAGE_SEARCH); + CopyBuffer(&CollationBuffers->Search, &Script); - CopyStringToBuffer(&CollationBuffers->Index, "%s", Script); + DeclaimBuffer(&PlayerURL); + DeclaimBuffer(&Script); + DeclaimBuffer(&URL); - if(!IndexRequired) { return RC_NOOP; } - else { return RC_SUCCESS; } - } - else - { - return RC_ERROR_FILE; - } -} - -char * -StripTrailingSlash(char *String) // NOTE(matt): For absolute paths -{ - int Length = StringLength(String); - while(Length > 0 && String[Length - 1] == '/') - { - String[Length - 1] = '\0'; - --Length; - } - return String; -} - -char * -StripSurroundingSlashes(char *String) // NOTE(matt): For relative paths -{ - int Length = StringLength(String); - if(Length > 0) - { - while((String[0]) == '/') - { - ++String; - --Length; - } - while(String[Length - 1] == '/') - { - String[Length - 1] = '\0'; - --Length; + if(!SearchRequired) { return RC_NOOP; } + else { return RC_SUCCESS; } } } - return String; + return RC_NOOP; } int -GeneratePlayerPage(index *Index, neighbourhood *N, buffers *CollationBuffers, template *PlayerTemplate, char *BaseFilename) +GeneratePlayerPage(neighbourhood *N, buffers *CollationBuffers, template *PlayerTemplate, char *BaseFilename, bool Reinserting) { buffer OutputDirectoryPath; - ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", 1024); + ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10); ConstructDirectoryPath(&OutputDirectoryPath, PAGE_PLAYER, Config.PlayerLocation, BaseFilename); DIR *OutputDirectoryHandle; @@ -5690,38 +7631,36 @@ GeneratePlayerPage(index *Index, neighbourhood *N, buffers *CollationBuffers, te CopyString(PlayerPagePath, sizeof(PlayerPagePath), "%s/index.html", OutputDirectoryPath.Location); DeclaimBuffer(&OutputDirectoryPath); - bool IndexInTemplate = FALSE; + bool SearchInTemplate = FALSE; for(int TagIndex = 0; TagIndex < PlayerTemplate->Metadata.TagCount; ++TagIndex) { - if(PlayerTemplate->Metadata.Tag[TagIndex].TagCode == TAG_INDEX) + if(PlayerTemplate->Metadata.Tag[TagIndex].TagCode == TAG_SEARCH) { - IndexInTemplate = TRUE; - IndexToBuffer(Index, CollationBuffers); + SearchInTemplate = TRUE; + SearchToBuffer(CollationBuffers); break; } } BuffersToHTML(CollationBuffers, PlayerTemplate, PlayerPagePath, PAGE_PLAYER, &N->This.LinkOffsets.PrevStart); + // NOTE(matt): A previous InsertNeighbourLink() call will have done SnipeEntryIntoMetadataBuffer(), but we must do it here now + // that PrevStart has been adjusted by BuffersToHTML() + SnipeEntryIntoMetadataBuffer(&N->This, N->ThisIndex); + UpdateLandmarksForNeighbourhood(N, Reinserting ? EDIT_REINSERTION : EDIT_ADDITION); - *(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * N->ThisIndex) = N->This; - - Index->Metadata.Handle = fopen(Index->Metadata.Path, "w"); - fwrite(Index->Metadata.Buffer.Location, Index->Metadata.Buffer.Size, 1, Index->Metadata.Handle); - fclose(Index->Metadata.Handle); - - if(IndexInTemplate) + if(SearchInTemplate) { - FreeBuffer(&CollationBuffers->Index); + FreeBuffer(&CollationBuffers->Search); } return RC_SUCCESS; } int -GenerateIndexPage(index *Index, buffers *CollationBuffers, template *IndexTemplate) +GenerateSearchPage(buffers *CollationBuffers, template *SearchTemplate) { buffer OutputDirectoryPath; - ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", 1024); - ConstructDirectoryPath(&OutputDirectoryPath, PAGE_INDEX, Config.IndexLocation, 0); + ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + 10); + ConstructDirectoryPath(&OutputDirectoryPath, PAGE_SEARCH, Config.SearchLocation, 0); DIR *OutputDirectoryHandle; if(!(OutputDirectoryHandle = opendir(OutputDirectoryPath.Location))) // TODO(matt): open() @@ -5735,55 +7674,56 @@ GenerateIndexPage(index *Index, buffers *CollationBuffers, template *IndexTempla } closedir(OutputDirectoryHandle); - char IndexPagePath[1024]; - CopyString(IndexPagePath, sizeof(IndexPagePath), "%s/index.html", OutputDirectoryPath.Location); + char SearchPagePath[1024]; + CopyString(SearchPagePath, sizeof(SearchPagePath), "%s/index.html", OutputDirectoryPath.Location); DeclaimBuffer(&OutputDirectoryPath); - switch(IndexToBuffer(Index, CollationBuffers)) + switch(SearchToBuffer(CollationBuffers)) { case RC_SUCCESS: { - BuffersToHTML(CollationBuffers, IndexTemplate, IndexPagePath, PAGE_INDEX, 0); + BuffersToHTML(CollationBuffers, SearchTemplate, SearchPagePath, PAGE_SEARCH, 0); + UpdateLandmarksForSearch(); break; } case RC_NOOP: { - DeleteIndexPageFromFilesystem(); + DeleteSearchPageFromFilesystem(); + DeleteLandmarksForSearch(); break; } } - FreeBuffer(&CollationBuffers->Index); + FreeBuffer(&CollationBuffers->Search); return RC_SUCCESS; } int -DeleteEntry(index *Index, neighbourhood *Neighbourhood, char *BaseFilename) +DeleteEntry(neighbourhood *Neighbourhood, char *BaseFilename) { - if(DeleteFromIndex(Index, Neighbourhood, BaseFilename) == RC_SUCCESS) + if(DeleteFromDB(Neighbourhood, BaseFilename) == RC_SUCCESS) { - if(Neighbourhood->This.Size > 0) - { - LinkNeighbours(Index, Neighbourhood, BaseFilename, LINK_EXCLUDE); - } + LinkNeighbours(Neighbourhood, LINK_EXCLUDE); DeletePlayerPageFromFilesystem(BaseFilename, Config.PlayerLocation, FALSE); + UpdateLandmarksForNeighbourhood(Neighbourhood, EDIT_DELETION); return RC_SUCCESS; } return RC_NOOP; } int -InsertEntry(index *Index, neighbourhood *Neighbourhood, buffers *CollationBuffers, template *PlayerTemplate, template *BespokeTemplate, char *BaseFilename, bool RecheckingPrivacy) +InsertEntry(neighbourhood *Neighbourhood, buffers *CollationBuffers, template *PlayerTemplate, template *BespokeTemplate, char *BaseFilename, bool RecheckingPrivacy) { - if(InsertIntoIndex(Index, Neighbourhood, CollationBuffers, &BespokeTemplate, BaseFilename, RecheckingPrivacy) == RC_SUCCESS) + bool Reinserting = FALSE; + if(InsertIntoDB(Neighbourhood, CollationBuffers, &BespokeTemplate, BaseFilename, RecheckingPrivacy, &Reinserting) == RC_SUCCESS) { - LinkNeighbours(Index, Neighbourhood, BaseFilename, LINK_INCLUDE); + LinkNeighbours(Neighbourhood, LINK_INCLUDE); if(StringsDiffer(BespokeTemplate->Metadata.Filename, "")) { - GeneratePlayerPage(Index, Neighbourhood, CollationBuffers, BespokeTemplate, BaseFilename); + GeneratePlayerPage(Neighbourhood, CollationBuffers, BespokeTemplate, BaseFilename, Reinserting); DeclaimTemplate(BespokeTemplate); } else { - GeneratePlayerPage(Index, Neighbourhood, CollationBuffers, PlayerTemplate, BaseFilename); + GeneratePlayerPage(Neighbourhood, CollationBuffers, PlayerTemplate, BaseFilename, Reinserting); } return RC_SUCCESS; } @@ -5791,18 +7731,19 @@ InsertEntry(index *Index, neighbourhood *Neighbourhood, buffers *CollationBuffer } int -RecheckPrivacy(index *Index, buffers *CollationBuffers, template *IndexTemplate, template *PlayerTemplate, template *BespokeTemplate) +RecheckPrivacy(buffers *CollationBuffers, template *SearchTemplate, template *PlayerTemplate, template *BespokeTemplate) { - if(Index->Metadata.FileSize > 0) + if(DB.Metadata.FileSize > 0) { - Index->Header = *(index_header*)Index->Metadata.Buffer.Location; - index_metadata Entry = { }; + DB.Header = *(db_header *)DB.Metadata.Buffer.Location; + DB.EntriesHeader = *(db_header_entries *)(DB.Metadata.Buffer.Location + sizeof(DB.Header)); + db_entry Entry = { }; int PrivateEntryIndex = 0; - index_metadata PrivateEntries[Index->Header.EntryCount]; + db_entry PrivateEntries[DB.EntriesHeader.Count]; bool Inserted = FALSE; - for(int IndexEntry = 0; IndexEntry < Index->Header.EntryCount; ++IndexEntry) + for(int IndexEntry = 0; IndexEntry < DB.EntriesHeader.Count; ++IndexEntry) { - Entry = *(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * IndexEntry); + Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * IndexEntry); if(Entry.Size == 0) { PrivateEntries[PrivateEntryIndex] = Entry; @@ -5813,15 +7754,15 @@ RecheckPrivacy(index *Index, buffers *CollationBuffers, template *IndexTemplate, for(int i = 0; i < PrivateEntryIndex; ++i) { neighbourhood Neighbourhood = { }; - Neighbourhood.PrevIndex = -1; - Neighbourhood.ThisIndex = -1; - Neighbourhood.NextIndex = -1; - Inserted = (InsertEntry(Index, &Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, PrivateEntries[i].BaseFilename, TRUE) == RC_SUCCESS); + InitNeighbourhood(&Neighbourhood); + ResetAssetLandmarks(); + Inserted = (InsertEntry(&Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, PrivateEntries[i].BaseFilename, TRUE) == RC_SUCCESS); } if(Inserted) { - GenerateIndexPage(Index, CollationBuffers, IndexTemplate); + GenerateSearchPage(CollationBuffers, SearchTemplate); + DeleteStaleAssets(); } LastPrivacyCheck = time(0); @@ -5829,74 +7770,275 @@ RecheckPrivacy(index *Index, buffers *CollationBuffers, template *IndexTemplate, return RC_SUCCESS; } -int -MonitorDirectory(index *Index, buffers *CollationBuffers, template *IndexTemplate, template *PlayerTemplate, template *BespokeTemplate, int inotifyInstance, int WatchDescriptor) +#define DEBUG_LANDMARKS 0 +#if DEBUG_LANDMARKS +void +VerifyLandmarks(void) { + printf("\n" + "VerifyLandmarks()\n"); + DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location; + DB.Header = *(db_header *)DB.Metadata.Buffer.Ptr; + DB.Metadata.Buffer.Ptr += sizeof(DB.Header); + DB.EntriesHeader = *(db_header_entries *)DB.Metadata.Buffer.Ptr; + DB.Metadata.Buffer.Ptr += sizeof(DB.EntriesHeader); + char *FirstEntry = DB.Metadata.Buffer.Ptr; -#if DEBUG_MEM - FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); - fprintf(MemLog, "\nCalled MonitorDirectory()\n"); - fclose(MemLog); -#endif + DB.Metadata.Buffer.Ptr += sizeof(DB.Entry) * DB.EntriesHeader.Count; + DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr; + DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader); + char *FirstAsset = DB.Metadata.Buffer.Ptr; - buffer Events; - if(ClaimBuffer(&Events, "inotify Events", Kilobytes(4)) == RC_ARENA_FULL) { return RC_ARENA_FULL; }; + buffer OutputDirectoryPath; + ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10); - struct inotify_event *Event; - int BytesRead = read(inotifyInstance, Events.Location, Events.Size); // TODO(matt): Handle error EINVAL - if(inotifyInstance < 0) { perror("MonitorDirectory()"); } + bool UniversalSuccess = TRUE; - struct inotify_event *FinalFileEvents[1024]; - int FinalFileEventsCount = 0; - - // TODO(matt): Test this with longer update intervals, and combinations of events... - for(Events.Ptr = Events.Location; - Events.Ptr < Events.Location + BytesRead && Events.Ptr - Events.Location < Events.Size; - Events.Ptr += sizeof(struct inotify_event) + Event->len) + // Search { - Event = (struct inotify_event *)Events.Ptr; - char *Ptr = Event->name; - Ptr += (StringLength(Event->name) - StringLength(".hmml")); - if(!(StringsDiffer(Ptr, ".hmml"))) + file_buffer HTML; + ReadSearchPageIntoBuffer(&HTML); + char *Cursor = FirstAsset; + for(int j = 0; j < DB.AssetsHeader.Count; ++j) { - *Ptr = '\0'; - bool FoundEvent = FALSE; - int FinalFileEventsIndex; - for(FinalFileEventsIndex = 0; FinalFileEventsIndex < FinalFileEventsCount; ++FinalFileEventsIndex) + DB.Asset = *(db_asset *)Cursor; + Cursor += sizeof(DB.Asset); + landmark_range Target = BinarySearchForMetadataLandmark(Cursor, PAGE_TYPE_SEARCH, DB.Asset.LandmarkCount); + for(int k = 0; k < Target.Length; ++k) { - if(!StringsDiffer(FinalFileEvents[FinalFileEventsIndex]->name, Event->name)) + DB.Landmark = *(db_landmark *)(Cursor + sizeof(db_landmark) * (Target.First + k)); + bool Found = FALSE; + for(int l = 0; l < Assets.Count; ++l) { - FinalFileEvents[FinalFileEventsIndex] = Event; - FoundEvent = TRUE; + // TODO(matt): StringToInt (base16) + char HashString[9]; + sprintf(HashString, "%08x", Assets.Asset[l].Hash); + + if(HTML.Buffer.Size >= DB.Landmark.Position + 8 && !StringsDifferT(HashString, HTML.Buffer.Location + DB.Landmark.Position, 0)) + { + Found = TRUE; + break; + } + } + if(!Found) + { + printf("Failure!!!\n"); + printf(" %s\n", HTML.Path); + printf(" %.*s [at byte %d] is not a known asset hash\n", 8, HTML.Buffer.Location + DB.Landmark.Position, DB.Landmark.Position); + UniversalSuccess = FALSE; } } - if(!FoundEvent) { FinalFileEvents[FinalFileEventsIndex] = Event; ++FinalFileEventsCount; } + Cursor += sizeof(DB.Landmark) * DB.Asset.LandmarkCount; } + FreeBuffer(&HTML.Buffer); } - bool Deleted = FALSE; - bool Inserted = FALSE; - for(int FinalFileEventsIndex = 0; FinalFileEventsIndex < FinalFileEventsCount; ++FinalFileEventsIndex) + for(int i = 0; i < DB.EntriesHeader.Count; ++i) { - neighbourhood Neighbourhood = { }; - Neighbourhood.PrevIndex = -1; - Neighbourhood.ThisIndex = -1; - Neighbourhood.NextIndex = -1; - - // TODO(matt): Maybe handle IN_ALL_EVENTS - if(FinalFileEvents[FinalFileEventsIndex]->mask & IN_DELETE || FinalFileEvents[FinalFileEventsIndex]->mask & IN_MOVED_FROM) + DB.Entry = *(db_entry *)(FirstEntry + sizeof(DB.Entry) * i); + ConstructDirectoryPath(&OutputDirectoryPath, PAGE_PLAYER, Config.PlayerLocation, DB.Entry.BaseFilename); + file_buffer HTML; + CopyString(HTML.Path, sizeof(HTML.Path), "%s/index.html", OutputDirectoryPath.Location); + if(ReadFileIntoBuffer(&HTML, 0) == RC_SUCCESS) { - Deleted |= (DeleteEntry(Index, &Neighbourhood, FinalFileEvents[FinalFileEventsIndex]->name) == RC_SUCCESS); + char *Cursor = FirstAsset; + for(int j = 0; j < DB.AssetsHeader.Count; ++j) + { + DB.Asset = *(db_asset *)Cursor; + Cursor += sizeof(DB.Asset); + landmark_range Target = BinarySearchForMetadataLandmark(Cursor, i, DB.Asset.LandmarkCount); + for(int k = 0; k < Target.Length; ++k) + { + DB.Landmark = *(db_landmark *)(Cursor + sizeof(db_landmark) * (Target.First + k)); + bool Found = FALSE; + for(int l = 0; l < Assets.Count; ++l) + { + // TODO(matt): StringToInt (base16) + char HashString[9]; + sprintf(HashString, "%08x", Assets.Asset[l].Hash); + + if((HTML.Buffer.Size >= DB.Landmark.Position + 8) && !StringsDifferT(HashString, HTML.Buffer.Location + DB.Landmark.Position, 0)) + { + Found = TRUE; + break; + } + } + if(!Found) + { + printf("%sFailure ↓%s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END]); + printf(" %s\n", HTML.Path); + printf(" %.*s [at byte %d] is not a known asset hash\n", 8, HTML.Buffer.Location + DB.Landmark.Position, DB.Landmark.Position); + UniversalSuccess = FALSE; + } + } + Cursor += sizeof(DB.Landmark) * DB.Asset.LandmarkCount; + } + FreeBuffer(&HTML.Buffer); } else { - Inserted |= (InsertEntry(Index, &Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, FinalFileEvents[FinalFileEventsIndex]->name, 0) == RC_SUCCESS); + printf("%sFailed to open%s %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], HTML.Path); + } + } + Assert(UniversalSuccess); + printf("Success! All landmarks correspond to asset hashes\n"); + DeclaimBuffer(&OutputDirectoryPath); +} +#endif + +void +UpdateDeferredAssetChecksums(void) +{ + for(int i = 0; i < Assets.Count; ++i) + { + if(Assets.Asset[i].DeferredUpdate) + { + UpdateAssetInDB(i); + } + } +} + +#define DEBUG_EVENTS 0 +#if DEBUG_EVENTS +void +PrintEvent(struct inotify_event *Event, int EventIndex) +{ + + printf("\nEvent[%d]\n" + " wd: %d\n" + " mask: %d\n", + EventIndex, + Event->wd, + Event->mask); + + if(Event->mask & IN_ACCESS) { printf(" IN_ACCESS\n"); } + if(Event->mask & IN_ATTRIB) { printf(" IN_ATTRIB\n"); } + if(Event->mask & IN_CLOSE_WRITE) { printf(" IN_CLOSE_WRITE\n"); } + if(Event->mask & IN_CLOSE_NOWRITE) { printf(" IN_CLOSE_NOWRITE\n"); } + if(Event->mask & IN_CREATE) { printf(" IN_CREATE\n"); } + if(Event->mask & IN_DELETE) { printf(" IN_DELETE\n"); } + if(Event->mask & IN_DELETE_SELF) { printf(" IN_DELETE_SELF\n"); } + if(Event->mask & IN_MODIFY) { printf(" IN_MODIFY\n"); } + if(Event->mask & IN_MOVE_SELF) { printf(" IN_MOVE_SELF\n"); } + if(Event->mask & IN_MOVED_FROM) { printf(" IN_MOVED_FROM\n"); } + if(Event->mask & IN_MOVED_TO) { printf(" IN_MOVED_TO\n"); } + if(Event->mask & IN_OPEN) { printf(" IN_OPEN\n"); } + if(Event->mask & IN_MOVE) { printf(" IN_MOVE\n"); } + if(Event->mask & IN_CLOSE) { printf(" IN_CLOSE\n"); } + if(Event->mask & IN_DONT_FOLLOW) { printf(" IN_DONT_FOLLOW\n"); } + if(Event->mask & IN_EXCL_UNLINK) { printf(" IN_EXCL_UNLINK\n"); } + if(Event->mask & IN_MASK_ADD) { printf(" IN_MASK_ADD\n"); } + if(Event->mask & IN_ONESHOT) { printf(" IN_ONESHOT\n"); } + if(Event->mask & IN_ONLYDIR) { printf(" IN_ONLYDIR\n"); } + if(Event->mask & IN_IGNORED) { printf(" IN_IGNORED\n"); } + if(Event->mask & IN_ISDIR) { printf(" IN_ISDIR\n"); } + if(Event->mask & IN_Q_OVERFLOW) { printf(" IN_Q_OVERFLOW\n"); } + if(Event->mask & IN_UNMOUNT) { printf(" IN_UNMOUNT\n"); } + + printf( " cookie: %d\n" + " len: %d\n" + " name: %s\n", + Event->cookie, + Event->len, + Event->name); +} +#endif + +int +MonitorFilesystem(buffers *CollationBuffers, template *SearchTemplate, template *PlayerTemplate, template *BespokeTemplate) +{ + buffer Events; + if(ClaimBuffer(&Events, "inotify Events", Kilobytes(4)) == RC_ARENA_FULL) { return RC_ARENA_FULL; }; + Clear(Events.Location, Events.Size); + + struct inotify_event *Event; + int BytesRead = read(inotifyInstance, Events.Location, Events.Size); // TODO(matt): Handle error EINVAL + if(inotifyInstance < 0) { perror("MonitorFilesystem()"); } + + // TODO(matt): Test this with longer update intervals, and combinations of events... +#if DEBUG_EVENTS + int i = 0; +#endif + bool Deleted = FALSE; + bool Inserted = FALSE; + bool UpdatedAsset = FALSE; + + for(Events.Ptr = Events.Location; + Events.Ptr - Events.Location < BytesRead && Events.Ptr - Events.Location < Events.Size; + Events.Ptr += sizeof(struct inotify_event) + Event->len +#if DEBUG_EVENTS + , ++i +#endif + ) + { + Event = (struct inotify_event *)Events.Ptr; + +#if DEBUG_EVENTS + PrintEvent(Event, i); +#endif + //PrintWatchHandles(); + + for(int WatchHandleIndex = 0; WatchHandleIndex < WatchHandles.Count; ++WatchHandleIndex) + { + if(Event->wd == WatchHandles.Handle[WatchHandleIndex].Descriptor) + { + switch(WatchHandles.Handle[WatchHandleIndex].Type) + { + case WT_HMML: + { + char *Ptr = Event->name; + Ptr += (StringLength(Event->name) - StringLength(".hmml")); + if(!(StringsDiffer(Ptr, ".hmml"))) + { + *Ptr = '\0'; + + neighbourhood Neighbourhood = { }; + InitNeighbourhood(&Neighbourhood); + ResetAssetLandmarks(); + + if(Event->mask & IN_DELETE) + { + Deleted |= (DeleteEntry(&Neighbourhood, Event->name) == RC_SUCCESS); + } + else if(Event->mask & IN_CLOSE_WRITE) + { + Inserted |= (InsertEntry(&Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, Event->name, 0) == RC_SUCCESS); + } + } + } break; + case WT_ASSET: + { + for(int i = 0; i < Assets.Count; ++i) + { + if(!StringsDiffer(Event->name, Assets.Asset[i].Filename + Assets.Asset[i].FilenameAt)) + { + UpdateAsset(i, 0); + UpdatedAsset = TRUE; + break; + } + } + } break; + } + break; + } } } if(Deleted || Inserted) { - GenerateIndexPage(Index, CollationBuffers, IndexTemplate); + UpdateDeferredAssetChecksums(); + GenerateSearchPage(CollationBuffers, SearchTemplate); + DeleteStaleAssets(); +#if DEBUG_LANDMARKS + VerifyLandmarks(); +#endif + } + + if(UpdatedAsset) + { +#if DEBUG_LANDMARKS + VerifyLandmarks(); +#endif } DeclaimBuffer(&Events); @@ -5909,13 +8051,13 @@ RemoveDirectory(char *Path) if((remove(Path) == -1)) { LogError(LOG_NOTICE, "Unable to remove directory %s: %s", Path, strerror(errno)); - fprintf(stderr, "\e[1;30mUnable to remove directory\e[0m %s: %s\n", Path, strerror(errno)); + fprintf(stderr, "%sUnable to remove directory%s %s: %s\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], Path, strerror(errno)); return RC_ERROR_DIRECTORY; } else { LogError(LOG_INFORMATIONAL, "Deleted %s", Path); - //fprintf(stderr, "\e[1;30mDeleted\e[0m %s\n", Path); + //fprintf(stderr, "%sDeleted%s %s\n", ColourStrings[CS_DELETION], ColourStrings[CS_END], Path); return RC_SUCCESS; } } @@ -5938,143 +8080,173 @@ RemoveDirectoryRecursively(char *Path) } int -UpgradeDB(index *Index) +UpgradeDB(int OriginalDBVersion) { - // DBVersion 1 - typedef struct { unsigned int DBVersion; version AppVersion; version HMMLVersion; unsigned int EntryCount; } index_header1; - typedef struct { int Size; char BaseFilename[32]; } index_metadata1; - typedef struct { file_buffer File; file_buffer Metadata; index_header1 Header; index_metadata1 Entry; } index1; - // - - // DBVersion 2 - typedef struct { unsigned int DBVersion; version AppVersion; version HMMLVersion; unsigned int EntryCount; char IndexLocation[32]; char PlayerLocation[32]; } index_header2; - typedef struct { int Size; char BaseFilename[32]; } index_metadata2; - typedef struct { file_buffer File; file_buffer Metadata; index_header2 Header; index_metadata2 Entry; } index2; - // - // NOTE(matt): For each new DB version, we must declare and initialise one instance of each preceding version, only cast the - // incoming Index to the type of the OriginalDBVersion, and move into the final case all operations on that incoming Index + // incoming DB to the type of the OriginalDBVersion, and move into the final case all operations on that incoming DB bool OnlyHeaderChanged = TRUE; int OriginalHeaderSize = 0; - index1 Index1 = { }; - index2 Index2 = { }; + database1 DB1 = { }; + database2 DB2 = { }; + database3 DB3 = { }; - int OriginalDBVersion = Index->Header.CurrentDBVersion; switch(OriginalDBVersion) { case 1: { - OriginalHeaderSize = sizeof(index_header1); - Index1.Header = *(index_header1 *)Index->Metadata.Buffer.Ptr; + OriginalHeaderSize = sizeof(db_header1); + DB1.Header = *(db_header1 *)DB.Metadata.Buffer.Location; - Index2.Header.DBVersion = CINERA_DB_VERSION; - Index2.Header.AppVersion = CINERA_APP_VERSION; - Index2.Header.HMMLVersion.Major = hmml_version.Major; - Index2.Header.HMMLVersion.Minor = hmml_version.Minor; - Index2.Header.HMMLVersion.Patch = hmml_version.Patch; - Index2.Header.EntryCount = Index1.Header.EntryCount; + DB2.Header.DBVersion = CINERA_DB_VERSION; + DB2.Header.AppVersion = CINERA_APP_VERSION; + DB2.Header.HMMLVersion.Major = hmml_version.Major; + DB2.Header.HMMLVersion.Minor = hmml_version.Minor; + DB2.Header.HMMLVersion.Patch = hmml_version.Patch; + DB2.Header.EntryCount = DB1.Header.EntryCount; - Clear(Index2.Header.IndexLocation, sizeof(Index2.Header.IndexLocation)); - Clear(Index2.Header.PlayerLocation, sizeof(Index2.Header.PlayerLocation)); + Clear(DB2.Header.SearchLocation, sizeof(DB2.Header.SearchLocation)); + Clear(DB2.Header.PlayerLocation, sizeof(DB2.Header.PlayerLocation)); } case 2: { if(OriginalDBVersion == 2) { - OriginalHeaderSize = sizeof(index_header2); - Index2.Header = *(index_header2 *)Index->Metadata.Buffer.Ptr; + OriginalHeaderSize = sizeof(db_header2); + DB2.Header = *(db_header2 *)DB.Metadata.Buffer.Location; + + DB.Header.InitialDBVersion = DB2.Header.DBVersion; + DB.Header.InitialAppVersion = DB2.Header.AppVersion; + DB.Header.InitialHMMLVersion.Major = DB2.Header.HMMLVersion.Major; + DB.Header.InitialHMMLVersion.Minor = DB2.Header.HMMLVersion.Minor; + DB.Header.InitialHMMLVersion.Patch = DB2.Header.HMMLVersion.Patch; + } - Index->Header.InitialDBVersion = Index2.Header.DBVersion; - Index->Header.InitialAppVersion = Index2.Header.AppVersion; - Index->Header.InitialHMMLVersion.Major = Index2.Header.HMMLVersion.Major; - Index->Header.InitialHMMLVersion.Minor = Index2.Header.HMMLVersion.Minor; - Index->Header.InitialHMMLVersion.Patch = Index2.Header.HMMLVersion.Patch; + DB.EntriesHeader.Count = DB2.Header.EntryCount; - Index->Header.CurrentDBVersion = CINERA_DB_VERSION; - Index->Header.CurrentAppVersion = CINERA_APP_VERSION; - Index->Header.CurrentHMMLVersion.Major = hmml_version.Major; - Index->Header.CurrentHMMLVersion.Minor = hmml_version.Minor; - Index->Header.CurrentHMMLVersion.Patch = hmml_version.Patch; + ClearCopyStringNoFormat(DB.EntriesHeader.ProjectID, sizeof(DB.EntriesHeader.ProjectID), Config.ProjectID); - Index->Header.EntryCount = Index2.Header.EntryCount; - - ClearCopyStringNoFormat(Index->Header.ProjectID, sizeof(Index->Header.ProjectID), Config.ProjectID); - - Clear(Index->Header.ProjectName, sizeof(Index->Header.ProjectName)); + Clear(DB.EntriesHeader.ProjectName, sizeof(DB.EntriesHeader.ProjectName)); for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex) { if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID)) { - CopyString(Index->Header.ProjectName, sizeof(Index->Header.ProjectName), "%s", ProjectInfo[ProjectIndex].FullName); + CopyString(DB.EntriesHeader.ProjectName, sizeof(DB.EntriesHeader.ProjectName), "%s", ProjectInfo[ProjectIndex].FullName); break; } } - ClearCopyStringNoFormat(Index->Header.BaseURL, sizeof(Index->Header.BaseURL), Config.BaseURL); - ClearCopyStringNoFormat(Index->Header.IndexLocation, sizeof(Index->Header.IndexLocation), Index2.Header.IndexLocation); - ClearCopyStringNoFormat(Index->Header.PlayerLocation, sizeof(Index->Header.PlayerLocation), Index2.Header.PlayerLocation); - ClearCopyStringNoFormat(Index->Header.PlayerURLPrefix, sizeof(Index->Header.PlayerURLPrefix), Config.PlayerURLPrefix); + ClearCopyStringNoFormat(DB.EntriesHeader.BaseURL, sizeof(DB.EntriesHeader.BaseURL), Config.BaseURL); + ClearCopyStringNoFormat(DB.EntriesHeader.SearchLocation, sizeof(DB.EntriesHeader.SearchLocation), DB2.Header.SearchLocation); + ClearCopyStringNoFormat(DB.EntriesHeader.PlayerLocation, sizeof(DB.EntriesHeader.PlayerLocation), DB2.Header.PlayerLocation); + ClearCopyStringNoFormat(DB.EntriesHeader.PlayerURLPrefix, sizeof(DB.EntriesHeader.PlayerURLPrefix), Config.PlayerURLPrefix); OnlyHeaderChanged = FALSE; - if(!(Index->Metadata.Handle = fopen(Index->Metadata.Path, "w"))) { FreeBuffer(&Index->Metadata.Buffer); return RC_ERROR_FILE; } - fwrite(&Index->Header, sizeof(Index->Header), 1, Index->Metadata.Handle); + if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; } + fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle); - Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + OriginalHeaderSize; - Index->File.Buffer.Ptr += StringLength("---\n"); + DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + OriginalHeaderSize; + DB.File.Buffer.Ptr += StringLength("---\n"); - for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex) + for(int EntryIndex = 0; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex) { - // NOTE(matt): We can use either index_metadata1 or 2 here because they are the same - index_metadata2 This = *(index_metadata2 *)Index->Metadata.Buffer.Ptr; + // NOTE(matt): We can use either db_entry1 or 2 here because they are the same + db_entry2 This = *(db_entry2 *)DB.Metadata.Buffer.Ptr; - Index->Entry.LinkOffsets.PrevStart = 0; - Index->Entry.LinkOffsets.NextStart = 0; - Index->Entry.LinkOffsets.PrevEnd = 0; - Index->Entry.LinkOffsets.NextEnd = 0; + DB.Entry.LinkOffsets.PrevStart = 0; + DB.Entry.LinkOffsets.NextStart = 0; + DB.Entry.LinkOffsets.PrevEnd = 0; + DB.Entry.LinkOffsets.NextEnd = 0; - Index->Entry.Size = This.Size; + DB.Entry.Size = This.Size; - ClearCopyStringNoFormat(Index->Entry.BaseFilename, sizeof(Index->Entry.BaseFilename), This.BaseFilename); + ClearCopyStringNoFormat(DB.Entry.BaseFilename, sizeof(DB.Entry.BaseFilename), This.BaseFilename); - char *IndexEntryStart = Index->File.Buffer.Ptr; - SeekBufferForString(&Index->File.Buffer, "title: \"", C_SEEK_FORWARDS, C_SEEK_AFTER); - Clear(Index->Entry.Title, sizeof(Index->Entry.Title)); - CopyStringNoFormatT(Index->Entry.Title, sizeof(Index->Entry.Title), Index->File.Buffer.Ptr, '\n'); - Index->Entry.Title[StringLength(Index->Entry.Title) - 1] = '\0'; + char *EntryStart = DB.File.Buffer.Ptr; + SeekBufferForString(&DB.File.Buffer, "title: \"", C_SEEK_FORWARDS, C_SEEK_AFTER); + Clear(DB.Entry.Title, sizeof(DB.Entry.Title)); + CopyStringNoFormatT(DB.Entry.Title, sizeof(DB.Entry.Title), DB.File.Buffer.Ptr, '\n'); + DB.Entry.Title[StringLength(DB.Entry.Title) - 1] = '\0'; - fwrite(&Index->Entry, sizeof(Index->Entry), 1, Index->Metadata.Handle); + fwrite(&DB.Entry, sizeof(DB.Entry), 1, DB.Metadata.Handle); - Index->Metadata.Buffer.Ptr += sizeof(This); - IndexEntryStart += This.Size; - Index->File.Buffer.Ptr = IndexEntryStart; + DB.Metadata.Buffer.Ptr += sizeof(This); + EntryStart += This.Size; + DB.File.Buffer.Ptr = EntryStart; } - fclose(Index->Metadata.Handle); - FreeBuffer(&Index->Metadata.Buffer); - if(ReadFileIntoBuffer(&Index->Metadata, 0) == RC_ERROR_FILE) - { - return RC_ERROR_FILE; - } + CycleFile(&DB.Metadata); } + case 3: + { + OriginalHeaderSize = sizeof(db_header3); + DB3.Header = *(db_header3 *)DB.Metadata.Buffer.Location; + DB.Header.HexSignature = FOURCC("CNRA"); + + DB.Header.CurrentDBVersion = CINERA_DB_VERSION; + DB.Header.CurrentAppVersion = CINERA_APP_VERSION; + DB.Header.CurrentHMMLVersion.Major = hmml_version.Major; + DB.Header.CurrentHMMLVersion.Minor = hmml_version.Minor; + DB.Header.CurrentHMMLVersion.Patch = hmml_version.Patch; + + DB.Header.InitialDBVersion = DB3.Header.InitialDBVersion; + DB.Header.InitialAppVersion = DB3.Header.InitialAppVersion; + DB.Header.InitialHMMLVersion.Major = DB3.Header.InitialHMMLVersion.Major; + DB.Header.InitialHMMLVersion.Minor = DB3.Header.InitialHMMLVersion.Minor; + DB.Header.InitialHMMLVersion.Patch = DB3.Header.InitialHMMLVersion.Patch; + DB.Header.BlockCount = 0; + + // TODO(matt): Consider allowing zero NTRY or ASET blocks in a future version + DB.EntriesHeader.BlockID = FOURCC("NTRY"); + DB.EntriesHeader.Count = DB3.Header.EntryCount; + ClearCopyStringNoFormat(DB.EntriesHeader.ProjectID, sizeof(DB.EntriesHeader.ProjectID), DB3.Header.ProjectID); + ClearCopyStringNoFormat(DB.EntriesHeader.ProjectName, sizeof(DB.EntriesHeader.ProjectName), DB3.Header.ProjectName); + ClearCopyStringNoFormat(DB.EntriesHeader.BaseURL, sizeof(DB.EntriesHeader.BaseURL), DB3.Header.BaseURL); + ClearCopyStringNoFormat(DB.EntriesHeader.SearchLocation, sizeof(DB.EntriesHeader.SearchLocation), DB3.Header.SearchLocation); + ClearCopyStringNoFormat(DB.EntriesHeader.PlayerLocation, sizeof(DB.EntriesHeader.PlayerLocation), DB3.Header.PlayerLocation); + ClearCopyStringNoFormat(DB.EntriesHeader.PlayerURLPrefix, sizeof(DB.EntriesHeader.PlayerURLPrefix), DB3.Header.PlayerURLPrefix); + ++DB.Header.BlockCount; + + DB.AssetsHeader.BlockID = FOURCC("ASET"); + DB.AssetsHeader.Count = 0; + ClearCopyStringNoFormat(DB.AssetsHeader.RootDir, sizeof(DB.AssetsHeader.RootDir), Config.RootDir); + ClearCopyStringNoFormat(DB.AssetsHeader.RootURL, sizeof(DB.AssetsHeader.RootURL), Config.RootURL); + ClearCopyStringNoFormat(DB.AssetsHeader.CSSDir, sizeof(DB.AssetsHeader.CSSDir), Config.CSSDir); + ClearCopyStringNoFormat(DB.AssetsHeader.ImagesDir, sizeof(DB.AssetsHeader.ImagesDir), Config.ImagesDir); + ClearCopyStringNoFormat(DB.AssetsHeader.JSDir, sizeof(DB.AssetsHeader.JSDir), Config.JSDir); + ++DB.Header.BlockCount; + + OnlyHeaderChanged = FALSE; + + if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; } + + fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle); + fwrite(&DB.EntriesHeader, sizeof(DB.EntriesHeader), 1, DB.Metadata.Handle); + fwrite(DB.Metadata.Buffer.Location + OriginalHeaderSize, + sizeof(DB.Entry), + DB.EntriesHeader.Count, + DB.Metadata.Handle); + + fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle); + + CycleFile(&DB.Metadata); + } + // TODO(matt); DBVersion 4 is the first that uses HexSignatures + // We should try and deprecate earlier versions and just enforce a clean rebuild for invalid DB files + // Perhaps do this either the next time we have to bump the version, or when we scrap Single Edition } if(OnlyHeaderChanged) { - if(!(Index->Metadata.Handle = fopen(Index->Metadata.Path, "w"))) { FreeBuffer(&Index->Metadata.Buffer); return RC_ERROR_FILE; } - fwrite(&Index->Header, sizeof(Index->Header), 1, Index->Metadata.Handle); - fwrite(Index->Metadata.Buffer.Location + OriginalHeaderSize, Index->Metadata.FileSize - OriginalHeaderSize, 1, Index->Metadata.Handle); - fclose(Index->Metadata.Handle); - FreeBuffer(&Index->Metadata.Buffer); - if(ReadFileIntoBuffer(&Index->Metadata, 0) == RC_ERROR_FILE) - { - return RC_ERROR_FILE; - } + if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; } + fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle); + fwrite(DB.Metadata.Buffer.Location + OriginalHeaderSize, DB.Metadata.FileSize - OriginalHeaderSize, 1, DB.Metadata.Handle); + CycleFile(&DB.Metadata); } - fprintf(stderr, "\n\e[1;32mUpgraded Cinera DB from %d to %d!\e[0m\n\n", OriginalDBVersion, Index->Header.CurrentDBVersion); + fprintf(stderr, "\n%sUpgraded Cinera DB from %d to %d!%s\n\n", ColourStrings[CS_SUCCESS], OriginalDBVersion, DB.Header.CurrentDBVersion, ColourStrings[CS_END]); return RC_SUCCESS; } @@ -6082,7 +8254,7 @@ typedef struct { bool Present; char ID[32]; -} index_entry; // Metadata, unless we actually want to bolster this? +} entry_presence_id; // Metadata, unless we actually want to bolster this? void RemoveChildDirectories(buffer FullPath, char *ParentDirectory) @@ -6101,101 +8273,83 @@ RemoveChildDirectories(buffer FullPath, char *ParentDirectory) } int -DeleteDeadIndexEntries(index *Index) +DeleteDeadDBEntries(void) { - // TODO(matt): More rigorously figure out who we should delete - // Maybe compare the output directory and the input HMML names - Index->Header = *(index_header *)Index->Metadata.Buffer.Location; - if(Index->Header.CurrentDBVersion < CINERA_DB_VERSION) - { - if(CINERA_DB_VERSION == 4) - { - fprintf(stderr, "\n\e[1;31mHandle conversion from CINERA_DB_VERSION %d to %d!\e[0m\n\n", Index->Header.CurrentDBVersion, CINERA_DB_VERSION); - exit(RC_ERROR_FATAL); - } - if(UpgradeDB(Index) == RC_ERROR_FILE) { return RC_NOOP; } - } - else if(Index->Header.CurrentDBVersion > CINERA_DB_VERSION) - { - fprintf(stderr, "\e[1;31mUnsupported DB Version (%d). Please upgrade Cinera\e[0m\n", Index->Header.CurrentDBVersion); - exit(RC_ERROR_FATAL); - } - bool NewPlayerLocation = FALSE; - bool NewIndexLocation = FALSE; - if(StringsDiffer(Index->Header.PlayerLocation, Config.PlayerLocation)) + bool NewSearchLocation = FALSE; + if(StringsDiffer(DB.EntriesHeader.PlayerLocation, Config.PlayerLocation)) { buffer OldPlayerDirectory; - ClaimBuffer(&OldPlayerDirectory, "OldPlayerDirectory", 1024); - ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, Index->Header.PlayerLocation, 0); + ClaimBuffer(&OldPlayerDirectory, "OldPlayerDirectory", + MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1); + ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, DB.EntriesHeader.PlayerLocation, 0); buffer NewPlayerDirectory; - ClaimBuffer(&NewPlayerDirectory, "NewPlayerDirectory", 1024); + ClaimBuffer(&NewPlayerDirectory, "NewPlayerDirectory", + MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1); ConstructDirectoryPath(&NewPlayerDirectory, PAGE_PLAYER, Config.PlayerLocation, 0); - printf("\e[1;33mRelocating Player Page%s from %s to %s\e[0m\n", - Index->Header.EntryCount > 1 ? "s" : "", - OldPlayerDirectory.Location, - NewPlayerDirectory.Location); + printf("%sRelocating Player Page%s from %s to %s%s\n", + ColourStrings[CS_REINSERTION], DB.EntriesHeader.Count > 1 ? "s" : "", + OldPlayerDirectory.Location, NewPlayerDirectory.Location, ColourStrings[CS_END]); DeclaimBuffer(&NewPlayerDirectory); - for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex) + for(int EntryIndex = 0; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex) { - index_metadata This = *(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex); - ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, Index->Header.PlayerLocation, This.BaseFilename); - DeletePlayerPageFromFilesystem(This.BaseFilename, Index->Header.PlayerLocation, TRUE); + db_entry This = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex); + ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, DB.EntriesHeader.PlayerLocation, This.BaseFilename); + DeletePlayerPageFromFilesystem(This.BaseFilename, DB.EntriesHeader.PlayerLocation, TRUE); } - ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, Index->Header.PlayerLocation, 0); + ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, DB.EntriesHeader.PlayerLocation, 0); - if(StringLength(Index->Header.PlayerLocation) > 0) + if(StringLength(DB.EntriesHeader.PlayerLocation) > 0) { RemoveChildDirectories(OldPlayerDirectory, Config.BaseDir); } DeclaimBuffer(&OldPlayerDirectory); - ClearCopyStringNoFormat(Index->Header.PlayerLocation, sizeof(Index->Header.PlayerLocation), Config.PlayerLocation); - *(index_header *)Index->Metadata.Buffer.Location = Index->Header; + ClearCopyStringNoFormat(DB.EntriesHeader.PlayerLocation, sizeof(DB.EntriesHeader.PlayerLocation), Config.PlayerLocation); + *(db_header *)DB.Metadata.Buffer.Location = DB.Header; NewPlayerLocation = TRUE; } - if(StringsDiffer(Index->Header.IndexLocation, Config.IndexLocation)) + if(StringsDiffer(DB.EntriesHeader.SearchLocation, Config.SearchLocation)) { - buffer OldIndexDirectory; - ClaimBuffer(&OldIndexDirectory, "OldIndexDirectory", 1024); - ConstructDirectoryPath(&OldIndexDirectory, PAGE_INDEX, Index->Header.IndexLocation, 0); - buffer NewIndexDirectory; - ClaimBuffer(&NewIndexDirectory, "NewIndexDirectory", 1024); - ConstructDirectoryPath(&NewIndexDirectory, PAGE_INDEX, Config.IndexLocation, 0); - printf("\e[1;33mRelocating Index Page from %s to %s\e[0m\n", - OldIndexDirectory.Location, - NewIndexDirectory.Location); - DeclaimBuffer(&NewIndexDirectory); + buffer OldSearchDirectory; + ClaimBuffer(&OldSearchDirectory, "OldSearchDirectory", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1); + ConstructDirectoryPath(&OldSearchDirectory, PAGE_SEARCH, DB.EntriesHeader.SearchLocation, 0); + buffer NewSearchDirectory; + ClaimBuffer(&NewSearchDirectory, "NewSearchDirectory", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1); + ConstructDirectoryPath(&NewSearchDirectory, PAGE_SEARCH, Config.SearchLocation, 0); + printf("%sRelocating Search Page from %s to %s%s\n", + ColourStrings[CS_REINSERTION], OldSearchDirectory.Location, NewSearchDirectory.Location, ColourStrings[CS_END]); + DeclaimBuffer(&NewSearchDirectory); - char IndexPagePath[2048] = { 0 }; - CopyString(IndexPagePath, sizeof(IndexPagePath), "%s/index.html", OldIndexDirectory.Location); - remove(IndexPagePath); - if(StringLength(Index->Header.IndexLocation) > 0) + char SearchPagePath[2048] = { 0 }; + CopyString(SearchPagePath, sizeof(SearchPagePath), "%s/index.html", OldSearchDirectory.Location); + remove(SearchPagePath); + if(StringLength(DB.EntriesHeader.SearchLocation) > 0) { - RemoveChildDirectories(OldIndexDirectory, Config.BaseDir); + RemoveChildDirectories(OldSearchDirectory, Config.BaseDir); } - DeclaimBuffer(&OldIndexDirectory); + DeclaimBuffer(&OldSearchDirectory); - ClearCopyStringNoFormat(Index->Header.IndexLocation, sizeof(Index->Header.IndexLocation), Config.IndexLocation); - *(index_header *)Index->Metadata.Buffer.Location = Index->Header; - NewIndexLocation = TRUE; + ClearCopyStringNoFormat(DB.EntriesHeader.SearchLocation, sizeof(DB.EntriesHeader.SearchLocation), Config.SearchLocation); + *(db_header *)DB.Metadata.Buffer.Location = DB.Header; + NewSearchLocation = TRUE; } - if(NewPlayerLocation || NewIndexLocation) + if(NewPlayerLocation || NewSearchLocation) { - if(!(Index->Metadata.Handle = fopen(Index->Metadata.Path, "w"))) { FreeBuffer(&Index->Metadata.Buffer); return RC_ERROR_FILE; } - fwrite(Index->Metadata.Buffer.Location, Index->Metadata.FileSize, 1, Index->Metadata.Handle); - fclose(Index->Metadata.Handle); + if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; } + fwrite(DB.Metadata.Buffer.Location, DB.Metadata.FileSize, 1, DB.Metadata.Handle); + CycleFile(&DB.Metadata); } - index_entry Entries[Index->Header.EntryCount]; + entry_presence_id Entries[DB.EntriesHeader.Count]; - for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex) + for(int EntryIndex = 0; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex) { - index_metadata This = *(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex); + db_entry This = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex); CopyStringNoFormat(Entries[EntryIndex].ID, sizeof(Entries[EntryIndex].ID), This.BaseFilename); Entries[EntryIndex].Present = FALSE; } @@ -6218,7 +8372,7 @@ DeleteDeadIndexEntries(index *Index) if(!(StringsDiffer(Ptr, ".hmml"))) { *Ptr = '\0'; - for(int i = 0; i < Index->Header.EntryCount; ++i) + for(int i = 0; i < DB.EntriesHeader.Count; ++i) { if(!StringsDiffer(Entries[i].ID, ProjectFiles->d_name)) { @@ -6231,16 +8385,15 @@ DeleteDeadIndexEntries(index *Index) closedir(ProjectDirHandle); bool Deleted = FALSE; - for(int i = 0; i < Index->Header.EntryCount; ++i) + for(int i = 0; i < DB.EntriesHeader.Count; ++i) { if(Entries[i].Present == FALSE) { Deleted = TRUE; neighbourhood Neighbourhood = { }; - Neighbourhood.PrevIndex = -1; - Neighbourhood.ThisIndex = -1; - Neighbourhood.NextIndex = -1; - DeleteEntry(Index, &Neighbourhood, Entries[i].ID); + InitNeighbourhood(&Neighbourhood); + ResetAssetLandmarks(); + DeleteEntry(&Neighbourhood, Entries[i].ID); } } @@ -6248,10 +8401,15 @@ DeleteDeadIndexEntries(index *Index) } int -SyncIndexWithInput(index *Index, buffers *CollationBuffers, template *IndexTemplate, template *PlayerTemplate, template *BespokeTemplate) +SyncDBWithInput(buffers *CollationBuffers, template *SearchTemplate, template *PlayerTemplate, template *BespokeTemplate) { + if(DB.Metadata.FileSize > 0 && Config.Mode & MODE_NOREVVEDRESOURCE) + { + DeleteStaleLandmarks(); + } + bool Deleted = FALSE; - Deleted = (Index->Metadata.FileSize > 0 && Index->File.FileSize > 0 && DeleteDeadIndexEntries(Index) == RC_SUCCESS); + Deleted = (DB.Metadata.FileSize > 0 && DB.File.FileSize > 0 && DeleteDeadDBEntries() == RC_SUCCESS); DIR *ProjectDirHandle; if(!(ProjectDirHandle = opendir(Config.ProjectDir))) @@ -6272,16 +8430,21 @@ SyncIndexWithInput(index *Index, buffers *CollationBuffers, template *IndexTempl { *Ptr = '\0'; neighbourhood Neighbourhood = { }; - Neighbourhood.PrevIndex = -1; - Neighbourhood.NextIndex = -1; - Inserted |= (InsertEntry(Index, &Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, ProjectFiles->d_name, 0) == RC_SUCCESS); + InitNeighbourhood(&Neighbourhood); + ResetAssetLandmarks(); + Inserted |= (InsertEntry(&Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, ProjectFiles->d_name, 0) == RC_SUCCESS); } } closedir(ProjectDirHandle); + UpdateDeferredAssetChecksums(); if(Deleted || Inserted) { - GenerateIndexPage(Index, CollationBuffers, IndexTemplate); + GenerateSearchPage(CollationBuffers, SearchTemplate); + DeleteStaleAssets(); +#if DEBUG_LANDMARKS + VerifyLandmarks(); +#endif } return RC_SUCCESS; } @@ -6300,6 +8463,193 @@ PrintVersions() CurlVersion->version); } +int +InitDB(void) +{ + DB.Metadata.Buffer.ID = "DBMetadata"; + CopyString(DB.Metadata.Path, sizeof(DB.Metadata.Path), "%s/%s.metadata", Config.BaseDir, Config.ProjectID); + ReadFileIntoBuffer(&DB.Metadata, 0); // NOTE(matt): Could we actually catch errors (permissions?) here and bail? + + DB.File.Buffer.ID = "DBFile"; + CopyString(DB.File.Path, sizeof(DB.File.Path), "%s/%s.index", Config.BaseDir, Config.ProjectID); + ReadFileIntoBuffer(&DB.File, 0); // NOTE(matt): Could we actually catch errors (permissions?) here and bail? + + if(DB.Metadata.Buffer.Location) + { + // TODO(matt): Handle this gracefully (it'll be an invalid file) + Assert(DB.Metadata.FileSize >= sizeof(DB.Header)); + uint32_t OriginalDBVersion = 0; + uint32_t FirstInt = *(uint32_t *)DB.Metadata.Buffer.Location; + if(FirstInt != FOURCC("CNRA")) + { + // TODO(matt): More checking, somehow? Ideally this should be able to report "Invalid .metadata file" rather than + // trying to upgrade a file that isn't even valid + // TODO(matt): We should try and deprecate < CINERA_DB_VERSION 4 and enforce a clean rebuild for invalid DB files + // Perhaps do this either the next time we have to bump the version, or when we scrap Single Edition + OriginalDBVersion = FirstInt; + } + else + { + OriginalDBVersion = *(uint32_t *)(DB.Metadata.Buffer.Location + sizeof(DB.Header.HexSignature)); + } + + if(OriginalDBVersion < CINERA_DB_VERSION) + { + if(CINERA_DB_VERSION == 5) + { + fprintf(stderr, "\n%sHandle conversion from CINERA_DB_VERSION %d to %d!%s\n\n", ColourStrings[CS_ERROR], OriginalDBVersion, CINERA_DB_VERSION, ColourStrings[CS_END]); + exit(RC_ERROR_FATAL); + } + if(UpgradeDB(OriginalDBVersion) == RC_ERROR_FILE) { return RC_NOOP; } + } + else if(OriginalDBVersion > CINERA_DB_VERSION) + { + fprintf(stderr, "%sUnsupported DB Version (%d). Please upgrade Cinera%s\n", ColourStrings[CS_ERROR], OriginalDBVersion, ColourStrings[CS_END]); + exit(RC_ERROR_FATAL); + } + + DB.Header = *(db_header *)DB.Metadata.Buffer.Location; + DB.Header.CurrentAppVersion = CINERA_APP_VERSION; + DB.Header.CurrentHMMLVersion.Major = hmml_version.Major; + DB.Header.CurrentHMMLVersion.Minor = hmml_version.Minor; + DB.Header.CurrentHMMLVersion.Patch = hmml_version.Patch; + + DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"); + fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle); + + DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header); + + // NOTE(matt): In the future we may want to support multiple occurrences of a block type, at which point this code + // should continue to work + for(int BlockIndex = 0; BlockIndex < DB.Header.BlockCount; ++BlockIndex) + { + uint32_t BlockID = *(uint32_t *)DB.Metadata.Buffer.Ptr; + if(BlockID == FOURCC("NTRY")) + { + DB.EntriesHeader = *(db_header_entries *)DB.Metadata.Buffer.Ptr; + fwrite(DB.Metadata.Buffer.Ptr, + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * DB.EntriesHeader.Count, + 1, + DB.Metadata.Handle); + DB.Metadata.Buffer.Ptr += sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * DB.EntriesHeader.Count; + } + else if(BlockID == FOURCC("ASET")) + { + DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr; + ClearCopyStringNoFormat(DB.AssetsHeader.RootDir, sizeof(DB.AssetsHeader.RootDir), Config.RootDir); + ClearCopyStringNoFormat(DB.AssetsHeader.RootURL, sizeof(DB.AssetsHeader.RootURL), Config.RootURL); + ClearCopyStringNoFormat(DB.AssetsHeader.CSSDir, sizeof(DB.AssetsHeader.CSSDir), Config.CSSDir); + ClearCopyStringNoFormat(DB.AssetsHeader.ImagesDir, sizeof(DB.AssetsHeader.ImagesDir), Config.ImagesDir); + ClearCopyStringNoFormat(DB.AssetsHeader.JSDir, sizeof(DB.AssetsHeader.JSDir), Config.JSDir); + fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle); + DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader); + for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex) + { + DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr; + fwrite(DB.Metadata.Buffer.Ptr, + sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount, + 1, + DB.Metadata.Handle); + DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount; + } + } + else + { + printf("%sMalformed metadata file%s: %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], DB.Metadata.Path); + } + } + + CycleFile(&DB.Metadata); + } + else + { + // NOTE(matt): Initialising new db_header + DB.Header.HexSignature = FOURCC("CNRA"); + DB.Header.InitialDBVersion = DB.Header.CurrentDBVersion = CINERA_DB_VERSION; + DB.Header.InitialAppVersion = DB.Header.CurrentAppVersion = CINERA_APP_VERSION; + DB.Header.InitialHMMLVersion.Major = DB.Header.CurrentHMMLVersion.Major = hmml_version.Major; + DB.Header.InitialHMMLVersion.Minor = DB.Header.CurrentHMMLVersion.Minor = hmml_version.Minor; + DB.Header.InitialHMMLVersion.Patch = DB.Header.CurrentHMMLVersion.Patch = hmml_version.Patch; + DB.Header.BlockCount = 0; + + CopyStringNoFormat(DB.EntriesHeader.ProjectID, sizeof(DB.EntriesHeader.ProjectID), Config.ProjectID); + + for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex) + { + if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID)) + { + CopyStringNoFormat(DB.EntriesHeader.ProjectName, sizeof(DB.EntriesHeader.ProjectName), ProjectInfo[ProjectIndex].FullName); + break; + } + } + + // TODO(matt): Consider allowing zero NTRY or ASET blocks in a future version + DB.EntriesHeader.BlockID = FOURCC("NTRY"); + DB.EntriesHeader.Count = 0; + CopyStringNoFormat(DB.EntriesHeader.BaseURL, sizeof(DB.EntriesHeader.BaseURL), Config.BaseURL); + CopyStringNoFormat(DB.EntriesHeader.SearchLocation, sizeof(DB.EntriesHeader.SearchLocation), Config.SearchLocation); + CopyStringNoFormat(DB.EntriesHeader.PlayerLocation, sizeof(DB.EntriesHeader.PlayerLocation), Config.PlayerLocation); + CopyStringNoFormat(DB.EntriesHeader.PlayerURLPrefix, sizeof(DB.EntriesHeader.PlayerURLPrefix), Config.PlayerURLPrefix); + ++DB.Header.BlockCount; + + DB.AssetsHeader.BlockID = FOURCC("ASET"); + DB.AssetsHeader.Count = 0; + CopyStringNoFormat(DB.AssetsHeader.RootDir, sizeof(DB.AssetsHeader.RootDir), Config.RootDir); + CopyStringNoFormat(DB.AssetsHeader.RootURL, sizeof(DB.AssetsHeader.RootURL), Config.RootURL); + CopyStringNoFormat(DB.AssetsHeader.CSSDir, sizeof(DB.AssetsHeader.CSSDir), Config.CSSDir); + CopyStringNoFormat(DB.AssetsHeader.ImagesDir, sizeof(DB.AssetsHeader.ImagesDir), Config.ImagesDir); + CopyStringNoFormat(DB.AssetsHeader.JSDir, sizeof(DB.AssetsHeader.JSDir), Config.JSDir); + ++DB.Header.BlockCount; + + DIR *OutputDirectoryHandle; + if(!(OutputDirectoryHandle = opendir(Config.BaseDir))) + { + if(MakeDir(Config.BaseDir) == RC_ERROR_DIRECTORY) + { + LogError(LOG_ERROR, "Unable to create directory %s: %s", Config.BaseDir, strerror(errno)); + fprintf(stderr, "Unable to create directory %s: %s\n", Config.BaseDir, strerror(errno)); + return RC_ERROR_DIRECTORY; + }; + } + closedir(OutputDirectoryHandle); + + DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"); + fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle); + fwrite(&DB.EntriesHeader, sizeof(DB.EntriesHeader), 1, DB.Metadata.Handle); + fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle); + fclose(DB.Metadata.Handle); + ReadFileIntoBuffer(&DB.Metadata, 0); + + DB.File.Handle = fopen(DB.File.Path, "w"); + fprintf(DB.File.Handle, "---\n"); + fclose(DB.File.Handle); + ReadFileIntoBuffer(&DB.File, 0); + } + return RC_SUCCESS; +} + +void +SetCacheDirectory(config *DefaultConfig) +{ + int Flags = WRDE_NOCMD | WRDE_UNDEF | WRDE_APPEND; + wordexp_t Expansions = {}; + wordexp("$XDG_CACHE_HOME/cinera", &Expansions, Flags); + wordexp("$HOME/.cache/cinera", &Expansions, Flags); + if(Expansions.we_wordc > 0 ) { CopyString(DefaultConfig->CacheDir, sizeof(DefaultConfig->CacheDir), Expansions.we_wordv[0]); } + wordfree(&Expansions); +} + +void +InitMemoryArena(arena *Arena, int Size) +{ + Arena->Size = Size; + if(!(Arena->Location = calloc(1, Arena->Size))) + { + exit(RC_ERROR_MEMORY); + } + Arena->Ptr = Arena->Location; +} + int main(int ArgC, char **Args) { @@ -6310,12 +8660,13 @@ main(int ArgC, char **Args) .CSSDir = "", .ImagesDir = "", .JSDir = "", + .QueryString = "r", .TemplatesDir = ".", - .TemplateIndexLocation = "", + .TemplateSearchLocation = "", .TemplatePlayerLocation = "", .BaseDir = ".", .BaseURL = "", - .IndexLocation = "", + .SearchLocation = "", .PlayerLocation = "", // Should default to the ProjectID .Edition = EDITION_SINGLE, .LogLevel = LOG_EMERGENCY, @@ -6330,14 +8681,7 @@ main(int ArgC, char **Args) .PlayerURLPrefix = "" }; - if(getenv("XDG_CACHE_HOME")) - { - CopyString(DefaultConfig.CacheDir, sizeof(DefaultConfig.CacheDir), "%s/cinera", getenv("XDG_CACHE_HOME")); - } - else - { - CopyString(DefaultConfig.CacheDir, sizeof(DefaultConfig.CacheDir), "%s/.cache/cinera", getenv("HOME")); - } + SetCacheDirectory(&DefaultConfig); Config = DefaultConfig; @@ -6348,7 +8692,7 @@ main(int ArgC, char **Args) } char CommandLineArg; - while((CommandLineArg = getopt(ArgC, Args, "1a:b:B:c:d:efghi:j:l:m:n:o:p:qr:R:s:t:u:vwx:y:")) != -1) + while((CommandLineArg = getopt(ArgC, Args, "1a:b:B:c:d:efghi:j:l:m:n:o:p:qQ:r:R:s:t:u:vwx:y:")) != -1) { switch(CommandLineArg) { @@ -6393,7 +8737,7 @@ main(int ArgC, char **Args) Config.DefaultMedium = optarg; break; case 'n': - Config.IndexLocation = StripSurroundingSlashes(optarg); + Config.SearchLocation = StripSurroundingSlashes(optarg); break; case 'o': Config.OutLocation = optarg; @@ -6405,6 +8749,13 @@ main(int ArgC, char **Args) case 'q': Config.Mode |= MODE_ONESHOT; break; + case 'Q': + Config.QueryString = optarg; + if(!StringsDiffer(Config.QueryString, "")) + { + Config.Mode |= MODE_NOREVVEDRESOURCE; + } + break; case 'r': Config.RootDir = StripTrailingSlash(optarg); break; @@ -6427,7 +8778,7 @@ main(int ArgC, char **Args) Config.Mode |= MODE_NOCACHE; break; case 'x': - Config.TemplateIndexLocation = optarg; + Config.TemplateSearchLocation = optarg; break; case 'y': Config.TemplatePlayerLocation = optarg; @@ -6441,22 +8792,14 @@ main(int ArgC, char **Args) if(Config.Mode & MODE_EXAMINE) { - index Index = { }; - Index.Metadata.Buffer.ID = "IndexMetadata"; // TODO(matt): Allow optionally passing a .metadata file as an argument? - CopyString(Index.Metadata.Path, sizeof(Index.Metadata.Path), "%s/%s.metadata", Config.BaseDir, Config.ProjectID); - ExamineIndex(&Index); + ExamineDB(); exit(RC_SUCCESS); } - // NOTE(matt): Init MemoryArena (it is global) - MemoryArena.Size = Megabytes(4); - if(!(MemoryArena.Location = calloc(MemoryArena.Size, 1))) - { - LogError(LOG_EMERGENCY, "%s: %s", Args[0], strerror(errno)); - return RC_RIP; - } - MemoryArena.Ptr = MemoryArena.Location; + // NOTE(matt): Init MemoryArenas (they are global) + InitMemoryArena(&MemoryArena, Megabytes(4)); + InitMemoryArena(&TemplateArena, Kilobytes(16)); // TODO(matt): Consider some way of making this growable #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); @@ -6475,8 +8818,8 @@ main(int ArgC, char **Args) // Player // ScriptPlayer // - // IncludesIndex - // Index + // IncludesSearch + // Search buffers CollationBuffers = {}; if(ClaimBuffer(&CollationBuffers.IncludesPlayer, "IncludesPlayer", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; }; @@ -6484,15 +8827,15 @@ main(int ArgC, char **Args) if(ClaimBuffer(&CollationBuffers.Player, "Player", Kilobytes(256)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&CollationBuffers.ScriptPlayer, "ScriptPlayer", Kilobytes(8)) == RC_ARENA_FULL) { goto RIP; }; - if(ClaimBuffer(&CollationBuffers.IncludesIndex, "IncludesIndex", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; }; - if(ClaimBuffer(&CollationBuffers.Search, "Search", Kilobytes(32)) == RC_ARENA_FULL) { goto RIP; }; + if(ClaimBuffer(&CollationBuffers.IncludesSearch, "IncludesSearch", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; }; + if(ClaimBuffer(&CollationBuffers.SearchEntry, "Search", Kilobytes(32)) == RC_ARENA_FULL) { goto RIP; }; bool HaveConfigErrors = FALSE; if(StringsDiffer(Config.ProjectID, "")) { if(StringLength(Config.ProjectID) > MAX_PROJECT_ID_LENGTH) { - fprintf(stderr, "\e[1;31mProjectID \"%s\" is too long (%d/%d characters)\e[0m\n", Config.ProjectID, StringLength(Config.ProjectID), MAX_PROJECT_ID_LENGTH); + fprintf(stderr, "%sProjectID \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], Config.ProjectID, StringLength(Config.ProjectID), MAX_PROJECT_ID_LENGTH, ColourStrings[CS_END]); HaveConfigErrors = TRUE; } @@ -6522,7 +8865,7 @@ main(int ArgC, char **Args) { if(StringLength(ProjectInfo[ProjectInfoIndex].AltURLPrefix) > MAX_PLAYER_URL_PREFIX_LENGTH) { - fprintf(stderr, "\e[1;31mPlayer URL Prefix \"%s\" is too long (%d/%d characters)\e[0m\n", ProjectInfo[ProjectInfoIndex].AltURLPrefix, StringLength(ProjectInfo[ProjectInfoIndex].AltURLPrefix), MAX_PLAYER_URL_PREFIX_LENGTH); + fprintf(stderr, "%sPlayer URL Prefix \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], ProjectInfo[ProjectInfoIndex].AltURLPrefix, StringLength(ProjectInfo[ProjectInfoIndex].AltURLPrefix), MAX_PLAYER_URL_PREFIX_LENGTH, ColourStrings[CS_END]); HaveConfigErrors = TRUE; } else @@ -6533,7 +8876,7 @@ main(int ArgC, char **Args) if(StringLength(ProjectInfo[ProjectInfoIndex].FullName) > MAX_PROJECT_NAME_LENGTH) { - fprintf(stderr, "\e[1;31mProject Name \"%s\" is too long (%d/%d characters)\e[0m\n", ProjectInfo[ProjectInfoIndex].FullName, StringLength(ProjectInfo[ProjectInfoIndex].FullName), MAX_PROJECT_NAME_LENGTH); + fprintf(stderr, "%sProject Name \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], ProjectInfo[ProjectInfoIndex].FullName, StringLength(ProjectInfo[ProjectInfoIndex].FullName), MAX_PROJECT_NAME_LENGTH, ColourStrings[CS_END]); HaveConfigErrors = TRUE; } @@ -6542,26 +8885,26 @@ main(int ArgC, char **Args) } if(!KnownProject) { - fprintf(stderr, "\e[1;31mMissing Project Info for %s\e[0m \e[1;30m(-p)\e[0m\n", Config.ProjectID); + fprintf(stderr, "%sMissing Project Info for %s%s %s(-p)%s\n", ColourStrings[CS_ERROR], Config.ProjectID, ColourStrings[CS_END], ColourStrings[CS_COMMENT], ColourStrings[CS_END]); HaveConfigErrors = TRUE; } } if(StringsDiffer(Config.BaseURL, "") && StringLength(Config.BaseURL) > MAX_BASE_URL_LENGTH) { - fprintf(stderr, "\e[1;31mBase URL \"%s\" is too long (%d/%d characters)\e[0m\n", Config.BaseURL, StringLength(Config.BaseURL), MAX_BASE_URL_LENGTH); + fprintf(stderr, "%sBase URL \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], Config.BaseURL, StringLength(Config.BaseURL), MAX_BASE_URL_LENGTH, ColourStrings[CS_END]); HaveConfigErrors = TRUE; } - if(StringsDiffer(Config.IndexLocation, "") && StringLength(Config.IndexLocation) > MAX_RELATIVE_PAGE_LOCATION_LENGTH) + if(StringsDiffer(Config.SearchLocation, "") && StringLength(Config.SearchLocation) > MAX_RELATIVE_PAGE_LOCATION_LENGTH) { - fprintf(stderr, "\e[1;31mRelative Index Page Location \"%s\" is too long (%d/%d characters)\e[0m\n", Config.IndexLocation, StringLength(Config.IndexLocation), MAX_RELATIVE_PAGE_LOCATION_LENGTH); + fprintf(stderr, "%sRelative Search Page Location \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], Config.SearchLocation, StringLength(Config.SearchLocation), MAX_RELATIVE_PAGE_LOCATION_LENGTH, ColourStrings[CS_END]); HaveConfigErrors = TRUE; } if(StringsDiffer(Config.PlayerLocation, "") && StringLength(Config.PlayerLocation) > MAX_RELATIVE_PAGE_LOCATION_LENGTH) { - fprintf(stderr, "\e[1;31mRelative Player Page Location \"%s\" is too long (%d/%d characters)\e[0m\n", Config.PlayerLocation, StringLength(Config.PlayerLocation), MAX_RELATIVE_PAGE_LOCATION_LENGTH); + fprintf(stderr, "%sRelative Player Page Location \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], Config.PlayerLocation, StringLength(Config.PlayerLocation), MAX_RELATIVE_PAGE_LOCATION_LENGTH, ColourStrings[CS_END]); HaveConfigErrors = TRUE; } @@ -6579,9 +8922,120 @@ main(int ArgC, char **Args) // App is running all the time, and picking up changes to the config as we go // If we find a new template, we first of all Init and Validate it // In our case here, we just want to straight up Init our three possible templates - // and Validate the Index and Player templates if their locations are set + // and Validate the Search and Player templates if their locations are set - template *IndexTemplate; InitTemplate(&IndexTemplate); + if(Config.Edition == EDITION_PROJECT) + { +#if DEBUG_MEM + FILE *MemLog = fopen("/home/matt/cinera_mem", "w+"); + fprintf(MemLog, "Entered Project Edition\n"); + fclose(MemLog); +#endif + + // TODO(matt): Also log these startup messages? + PrintVersions(); + printf( "\n" + "Universal\n" + " Cache Directory: %s(XDG_CACHE_HOME)%s\t%s\n" + " Update Interval: %s(-u)%s\t\t%d second%s\n" + "\n" + " Assets Root\n" + " Directory: %s(-r)%s\t\t\t%s\n" + " URL: %s(-R)%s\t\t\t%s\n" + " Paths relative to root\n" + " CSS: %s(-c)%s\t\t\t%s\n" + " Images: %s(-i)%s\t\t\t%s\n" + " JS: %s(-j)%s\t\t\t%s\n" + " Revved resources query string: %s(-Q)%s\t%s\n" + "\n" + "Project\n" + " ID: %s(-p)%s\t\t\t\t%s\n" + " Default Medium: %s(-m)%s\t\t%s\n" + " Style / Theme: %s(-s)%s\t\t\t%s\n" + "\n" + "Input Paths\n" + " Annotations Directory: %s(-d)%s\t\t%s\n" + " Templates Directory: %s(-t)%s\t\t%s\n" + " Search Template: %s(-x)%s\t\t%s\n" + " Player Template: %s(-y)%s\t\t%s\n" + "\n" + "Output Paths\n" + " Base\n" + " Directory: %s(-b)%s\t\t\t%s\n" + " URL: %s(-B)%s\t\t\t%s\n" + " Paths relative to base\n" + " Search Page: %s(-n)%s\t\t%s\n" + /* NOTE(matt): Here, I think, is where we'll split into sub-projects (...really?...) */ + " Player Page(s): %s(-a)%s\t\t%s\n" + " Player Page Prefix: %s(hardcoded)%s\t%s\n" + "\n" + "Modes\n" + " Single browser tab: %s(-1)%s\t\t%s\n" + " Force template integration: %s(-f)%s\t%s\n" + " Ignore video privacy status: %s(-g)%s\t%s\n" + " Quit after sync: %s(-q)%s\t\t%s\n" + " Force quote cache rebuild: %s(-w)%s\t%s\n" + "\n", + + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.CacheDir, + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.UpdateInterval, + Config.UpdateInterval == 1 ? "" : "s", + + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.RootDir, + ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.RootURL, "") ? Config.RootURL : "[empty]", + + ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.CSSDir, "") ? Config.CSSDir : "(same as root)", + ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.ImagesDir, "") ? Config.ImagesDir : "(same as root)", + ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.JSDir, "") ? Config.JSDir : "(same as root)", + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_NOREVVEDRESOURCE ? "[disabled]" : Config.QueryString, + + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.ProjectID, + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.DefaultMedium, + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Theme, + + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.ProjectDir, + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.TemplatesDir, + ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.TemplateSearchLocation, "") ? Config.TemplateSearchLocation : "[none set]", + ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.TemplatePlayerLocation, "") ? Config.TemplatePlayerLocation : "[none set]", + + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.BaseDir, + ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.BaseURL, "") ? Config.BaseURL : "[empty]", + ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.SearchLocation, "") ? Config.SearchLocation : "(same as base)", + ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.PlayerLocation, "") ? Config.PlayerLocation : "(directly descended from base)", + ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.PlayerURLPrefix, "") ? Config.PlayerURLPrefix : Config.ProjectID, + + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_SINGLETAB ? "on" : "off", + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_FORCEINTEGRATION ? "on" : "off", + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_NOPRIVACY ? "on" : "off", + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_ONESHOT ? "on" : "off", + ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_NOCACHE ? "on" : "off"); + + if((StringsDiffer(Config.SearchLocation, "") || StringsDiffer(Config.PlayerLocation, "")) + && StringLength(Config.BaseURL) == 0) + { + printf("%sPlease set a Project Base URL (-B) so we can output the Search / Player pages to\n" + "locations other than the defaults%s\n", + ColourStrings[CS_ERROR], ColourStrings[CS_END]); + return(RC_SUCCESS); + } +#if 0 + for(int i = 0; i < Assets.Count; ++i) + { + printf("%08x - %s\n", Assets.Asset[i].Hash, Assets.Asset[i].Filename); + } +#endif + + InitDB(); + inotifyInstance = inotify_init1(IN_NONBLOCK); + + printf("┌╼ Hashing assets ╾┐\n"); + // NOTE(matt): This had to happen before ValidateTemplate() because those guys may need to do PushAsset() and we must + // ensure that the builtin assets get placed correctly + InitAssets(); + } + + printf("┌╼ Packing templates ╾┐\n"); + template *SearchTemplate; InitTemplate(&SearchTemplate); template *PlayerTemplate; InitTemplate(&PlayerTemplate); template *BespokeTemplate; InitTemplate(&BespokeTemplate); @@ -6598,9 +9052,9 @@ main(int ArgC, char **Args) } } - if(Config.Edition == EDITION_PROJECT && StringsDiffer(Config.TemplateIndexLocation, "")) + if(Config.Edition == EDITION_PROJECT && StringsDiffer(Config.TemplateSearchLocation, "")) { - switch(ValidateTemplate(&IndexTemplate, Config.TemplateIndexLocation, TEMPLATE_INDEX)) + switch(ValidateTemplate(&SearchTemplate, Config.TemplateSearchLocation, TEMPLATE_SEARCH)) { case RC_INVALID_TEMPLATE: // Invalid template case RC_ERROR_MEMORY: // Could not allocate memory for template @@ -6613,121 +9067,25 @@ main(int ArgC, char **Args) if(Config.Edition == EDITION_PROJECT) { - -#if DEBUG_MEM - FILE *MemLog = fopen("/home/matt/cinera_mem", "w+"); - fprintf(MemLog, "Entered Project Edition\n"); - fclose(MemLog); -#endif - - // TODO(matt): Also log these startup messages? - PrintVersions(); - printf( "\n" - "Universal\n" - " Cache Directory: \e[1;30m(XDG_CACHE_HOME)\e[0m\t%s\n" - "\n" - " Root\n" - " Directory: \e[1;30m(-r)\e[0m\t\t\t%s\n" - " URL: \e[1;30m(-R)\e[0m\t\t\t%s\n" - " Paths relative to root\n" - " CSS: \e[1;30m(-c)\e[0m\t\t\t%s\n" - " Images: \e[1;30m(-i)\e[0m\t\t\t%s\n" - " JS: \e[1;30m(-j)\e[0m\t\t\t%s\n" - "\n" - "Project\n" - " ID: \e[1;30m(-p)\e[0m\t\t\t\t%s\n" - " Default Medium: \e[1;30m(-m)\e[0m\t\t%s\n" - " Style / Theme: \e[1;30m(-s)\e[0m\t\t\t%s\n" - "\n" - "Input Paths\n" - " Annotations Directory: \e[1;30m(-d)\e[0m\t\t%s\n" - " Templates Directory: \e[1;30m(-t)\e[0m\t\t%s\n" - " Index Template: \e[1;30m(-x)\e[0m\t\t%s\n" - " Player Template: \e[1;30m(-y)\e[0m\t\t%s\n" - "\n" - "Output Paths\n" - " Base\n" - " Directory: \e[1;30m(-b)\e[0m\t\t\t%s\n" - " URL: \e[1;30m(-B)\e[0m\t\t\t%s\n" - " Paths relative to base\n" - " Index Page: \e[1;30m(-n)\e[0m\t\t\t%s\n" - /* NOTE(matt): Here, I think, is where we'll split into sub-projects (...really?...) */ - " Player Page(s): \e[1;30m(-a)\e[0m\t\t%s\n" - " Player Page Prefix: \e[1;30m(hardcoded)\e[0m\t%s\n" - "\n" - "Modes\n" - " Single browser tab: \e[1;30m(-1)\e[0m\t\t%s\n" - " Force template integration: \e[1;30m(-f)\e[0m\t%s\n" - " Ignore video privacy status: \e[1;30m(-g)\e[0m\t%s\n" - " Quit after sync: \e[1;30m(-q)\e[0m\t\t%s\n" - " Force quote cache rebuild: \e[1;30m(-w)\e[0m\t%s\n" - "\n", - - Config.CacheDir, - - Config.RootDir, - StringsDiffer(Config.RootURL, "") ? Config.RootURL : "[empty]", - - StringsDiffer(Config.CSSDir, "") ? Config.CSSDir : "(same as root)", - StringsDiffer(Config.ImagesDir, "") ? Config.ImagesDir : "(same as root)", - StringsDiffer(Config.JSDir, "") ? Config.JSDir : "(same as root)", - - Config.ProjectID, - Config.DefaultMedium, - Config.Theme, - - Config.ProjectDir, - Config.TemplatesDir, - StringsDiffer(Config.TemplateIndexLocation, "") ? Config.TemplateIndexLocation : "[none set]", - StringsDiffer(Config.TemplatePlayerLocation, "") ? Config.TemplatePlayerLocation : "[none set]", - - Config.BaseDir, - StringsDiffer(Config.BaseURL, "") ? Config.BaseURL : "[empty]", - StringsDiffer(Config.IndexLocation, "") ? Config.IndexLocation : "(same as base)", - StringsDiffer(Config.PlayerLocation, "") ? Config.PlayerLocation : "(directly descended from base)", - StringsDiffer(Config.PlayerURLPrefix, "") ? Config.PlayerURLPrefix : Config.ProjectID, - - Config.Mode & MODE_SINGLETAB ? "on" : "off", - Config.Mode & MODE_FORCEINTEGRATION ? "on" : "off", - Config.Mode & MODE_NOPRIVACY ? "on" : "off", - Config.Mode & MODE_ONESHOT ? "on" : "off", - Config.Mode & MODE_NOCACHE ? "on" : "off"); - - if((StringsDiffer(Config.IndexLocation, "") || StringsDiffer(Config.PlayerLocation, "")) - && StringLength(Config.BaseURL) == 0) - { - printf("\e[1;33mPlease set a Project Base URL (-B) so we can output the Index / Player pages to\n" - "locations other than the defaults\e[0m\n"); - return(RC_SUCCESS); - } - - index Index = { }; - - Index.Metadata.Buffer.ID = "IndexMetadata"; - CopyString(Index.Metadata.Path, sizeof(Index.Metadata.Path), "%s/%s.metadata", Config.BaseDir, Config.ProjectID); - ReadFileIntoBuffer(&Index.Metadata, 0); // NOTE(matt): Could we actually catch errors (permissions?) here and bail? - - Index.File.Buffer.ID = "IndexFile"; - CopyString(Index.File.Path, sizeof(Index.File.Path), "%s/%s.index", Config.BaseDir, Config.ProjectID); - ReadFileIntoBuffer(&Index.File, 0); // NOTE(matt): Could we actually catch errors (permissions?) here and bail? - - printf("┌╼ Synchronising with Annotations Directory ╾┐\n"); - SyncIndexWithInput(&Index, &CollationBuffers, IndexTemplate, PlayerTemplate, BespokeTemplate); + printf("\n┌╼ Synchronising with Annotations Directory ╾┐\n"); + SyncDBWithInput(&CollationBuffers, SearchTemplate, PlayerTemplate, BespokeTemplate); if(Config.Mode & MODE_ONESHOT) { goto RIP; } - printf("\n┌╼ Monitoring Annotations Directory for \e[1;32mnew\e[0m, \e[1;33medited\e[0m and \e[1;30mdeleted\e[0m .hmml files ╾┐\n"); - int inotifyInstance = inotify_init1(IN_NONBLOCK); + printf("\n┌╼ Monitoring Annotations Directory for %snew%s, %sedited%s and %sdeleted%s .hmml files ╾┐\n", + ColourStrings[EditTypes[EDIT_ADDITION].Colour], ColourStrings[CS_END], + ColourStrings[EditTypes[EDIT_REINSERTION].Colour], ColourStrings[CS_END], + ColourStrings[EditTypes[EDIT_DELETION].Colour], ColourStrings[CS_END]); // NOTE(matt): Do we want to also watch IN_DELETE_SELF events? - int WatchDescriptor = inotify_add_watch(inotifyInstance, Config.ProjectDir, IN_CLOSE_WRITE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO); + PushHMMLWatchHandle(); - while(MonitorDirectory(&Index, &CollationBuffers, IndexTemplate, PlayerTemplate, BespokeTemplate, inotifyInstance, WatchDescriptor) != RC_ARENA_FULL) + while(MonitorFilesystem(&CollationBuffers, SearchTemplate, PlayerTemplate, BespokeTemplate) != RC_ARENA_FULL) { // TODO(matt): Refetch the quotes and rebuild player pages if needed // - // Every sixty mins, redownload the quotes and, I suppose, SyncIndexWithInput(). But here we still don't even know + // Every sixty mins, redownload the quotes and, I suppose, SyncDBWithInput(). But here we still don't even know // who the speaker is. To know, we'll probably have to store all quoted speakers in the project's .metadata. Maybe // postpone this for now, but we will certainly need this to happen // @@ -6737,13 +9095,14 @@ main(int ArgC, char **Args) // if(!(Config.Mode & MODE_NOPRIVACY) && time(0) - LastPrivacyCheck > 60 * 60 * 4) { - RecheckPrivacy(&Index, &CollationBuffers, IndexTemplate, PlayerTemplate, BespokeTemplate); + RecheckPrivacy(&CollationBuffers, SearchTemplate, PlayerTemplate, BespokeTemplate); } sleep(Config.UpdateInterval); } } else { + // TODO(matt): Get rid of Single Edition once and for all, probably for v1.0.0 if(optind == ArgC) { fprintf(stderr, "%s: requires at least one input .hmml file\n", Args[0]); @@ -6751,6 +9110,11 @@ main(int ArgC, char **Args) goto RIP; } + inotifyInstance = inotify_init1(IN_NONBLOCK); + + printf("┌╼ Hashing assets ╾┐\n"); + InitBuiltinAssets(); + NextFile: // TODO(matt): Just change the default output location so all these guys won't overwrite each other for(int FileIndex = optind; FileIndex < ArgC; ++FileIndex) @@ -6794,7 +9158,7 @@ NextFile: goto RIP; case RC_SUCCESS: #if 0 - fprintf(stdout, "\e[1;32mWritten\e[0m %s\n", HasBespokeTemplate ? Config.OutIntegratedLocation : Config.OutLocation); + fprintf(stdout, "%sWritten%s %s\n", HasBespokeTemplate ? Config.OutIntegratedLocation : Config.OutLocation); #endif if(HasBespokeTemplate) { DeclaimTemplate(BespokeTemplate); } break; @@ -6807,13 +9171,13 @@ NextFile: { DeclaimTemplate(PlayerTemplate); } - if(Config.Edition == EDITION_PROJECT && StringsDiffer(IndexTemplate->Metadata.Filename, "")) + if(Config.Edition == EDITION_PROJECT && StringsDiffer(SearchTemplate->Metadata.Filename, "")) { - DeclaimTemplate(IndexTemplate); + DeclaimTemplate(SearchTemplate); } - DeclaimBuffer(&CollationBuffers.Search); - DeclaimBuffer(&CollationBuffers.IncludesIndex); + DeclaimBuffer(&CollationBuffers.SearchEntry); + DeclaimBuffer(&CollationBuffers.IncludesSearch); DeclaimBuffer(&CollationBuffers.ScriptPlayer); DeclaimBuffer(&CollationBuffers.Player);