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);