#if 0 ctime -begin ${0%.*}.ctm #gcc -g -fsanitize=address -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl gcc -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl #clang -fsanitize=address -g -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl #clang -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl ctime -end ${0%.*}.ctm exit #endif #include typedef struct { uint32_t Major, Minor, Patch; } version; version CINERA_APP_VERSION = { .Major = 0, .Minor = 6, .Patch = 6 }; #include // NOTE(matt): varargs #include // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror #include // NOTE(matt): calloc, malloc, free #include "hmmlib.h" #include // NOTE(matt): getopts //#include "config.h" // TODO(matt): Implement config.h #include #include #include #include #include #include // NOTE(matt): strerror #include //NOTE(matt): errno #include // NOTE(matt): inotify #include #define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW #include // NOTE(matt): open() #define __USE_XOPEN2K // NOTE(matt): readlink() #include // 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 bool PROFILING = 0; clock_t TIMING_START; #define START_TIMING_BLOCK(...) if(PROFILING) { printf(__VA_ARGS__); TIMING_START = clock(); } #define END_TIMING_BLOCK() if(PROFILING) { printf("\e[1;34m%ld\e[0m\n", clock() - TIMING_START);} #define Kilobytes(Bytes) Bytes << 10 #define Megabytes(Bytes) Bytes << 20 #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_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, EDITION_PROJECT, EDITION_NETWORK } editions; enum { // NOTE(matt): https://tools.ietf.org/html/rfc5424#section-6.2.1 LOG_EMERGENCY, LOG_ALERT, LOG_CRITICAL, LOG_ERROR, LOG_WARNING, LOG_NOTICE, LOG_INFORMATIONAL, LOG_DEBUG } log_levels; enum { MODE_FORCEINTEGRATION = 1 << 0, MODE_ONESHOT = 1 << 1, MODE_EXAMINE = 1 << 2, MODE_NOCACHE = 1 << 3, MODE_NOPRIVACY = 1 << 4, MODE_SINGLETAB = 1 << 5, MODE_NOREVVEDRESOURCE = 1 << 6 } modes; enum { RC_ARENA_FULL, RC_ERROR_DIRECTORY, RC_ERROR_FATAL, RC_ERROR_FILE, RC_ERROR_HMML, RC_ERROR_MAX_REFS, RC_ERROR_MEMORY, RC_ERROR_PARSING, RC_ERROR_PROJECT, RC_ERROR_QUOTE, RC_ERROR_SEEK, RC_FOUND, RC_UNFOUND, RC_INVALID_REFERENCE, RC_INVALID_TEMPLATE, RC_PRIVATE_VIDEO, RC_NOOP, RC_RIP, RC_SUCCESS } returns; typedef struct { void *Location; void *Ptr; char *ID; int Size; } arena; typedef struct { // Universal 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 char *RootURL; 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; char *Theme; char *DefaultMedium; // Per Project - Input char *ProjectDir; // Absolute char *TemplatesDir; // Absolute char *TemplateSearchLocation; // Relative to TemplatesDir ??? char *TemplatePlayerLocation; // Relative to TemplatesDir ??? // Per Project - Output char *BaseDir; // Absolute char *BaseURL; 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 */ // Single Edition - Input char SingleHMMLFilePath[256]; // Single Edition - Output char *OutLocation; char *OutIntegratedLocation; } config; typedef struct { char *Location; char *Ptr; char *ID; int Size; } buffer; typedef struct { buffer Buffer; FILE *Handle; char Path[256]; // NOTE(matt): Could this just be a char *? 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 { unsigned int CurrentDBVersion; version CurrentAppVersion; version CurrentHMMLVersion; unsigned int InitialDBVersion; version InitialAppVersion; version InitialHMMLVersion; unsigned short int EntryCount; 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]; } db_header3; typedef struct { unsigned int PrevStart, NextStart; unsigned short int PrevEnd, NextEnd; } link_insertion_offsets; // NOTE(matt): PrevStart is Absolute (or relative to start of file), the others are Relative to PrevStart typedef struct { link_insertion_offsets LinkOffsets; unsigned short int Size; char BaseFilename[MAX_BASE_FILENAME_LENGTH + 1]; char Title[MAX_TITLE_LENGTH + 1]; } db_entry3; typedef struct { file_buffer File; file_buffer Metadata; 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 { 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; 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; buffer ScriptPlayer; char Custom0[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1]; char Custom1[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1]; char Custom2[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1]; char Custom3[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1]; char Custom4[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1]; char Custom5[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1]; char Custom6[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1]; char Custom7[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1]; char Custom8[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1]; char Custom9[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1]; char Custom10[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1]; char Custom11[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1]; char Custom12[MAX_CUSTOM_SNIPPET_LONG_LENGTH + 1]; char Custom13[MAX_CUSTOM_SNIPPET_LONG_LENGTH + 1]; char Custom14[MAX_CUSTOM_SNIPPET_LONG_LENGTH + 1]; char Custom15[MAX_CUSTOM_SNIPPET_LONG_LENGTH + 1]; char ProjectID[MAX_PROJECT_ID_LENGTH + 1]; char ProjectName[MAX_PROJECT_NAME_LENGTH + 1]; char Theme[MAX_PROJECT_NAME_LENGTH + 1]; char Title[MAX_TITLE_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]; char VODPlatform[16]; } buffers; enum { // Contents and Player Pages Mandatory TAG_INCLUDES, // Contents Page Mandatory TAG_SEARCH, // Player Page Mandatory TAG_MENUS, TAG_PLAYER, TAG_SCRIPT, // Player Page Optional TAG_CUSTOM0, TAG_CUSTOM1, TAG_CUSTOM2, TAG_CUSTOM3, TAG_CUSTOM4, TAG_CUSTOM5, TAG_CUSTOM6, TAG_CUSTOM7, TAG_CUSTOM8, TAG_CUSTOM9, TAG_CUSTOM10, TAG_CUSTOM11, TAG_CUSTOM12, TAG_CUSTOM13, TAG_CUSTOM14, TAG_CUSTOM15, TAG_TITLE, TAG_VIDEO_ID, TAG_VOD_PLATFORM, // Anywhere Optional TAG_ASSET, TAG_CSS, TAG_IMAGE, TAG_JS, TAG_PROJECT, TAG_PROJECT_ID, TAG_SEARCH_URL, TAG_THEME, TAG_URL, TEMPLATE_TAG_COUNT, } template_tag_codes; char *TemplateTags[] = { "__CINERA_INCLUDES__", "__CINERA_SEARCH__", "__CINERA_MENUS__", "__CINERA_PLAYER__", "__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__", "__CINERA_CUSTOM12__", "__CINERA_CUSTOM13__", "__CINERA_CUSTOM14__", "__CINERA_CUSTOM15__", "__CINERA_TITLE__", "__CINERA_VIDEO_ID__", "__CINERA_VOD_PLATFORM__", "__CINERA_ASSET__", "__CINERA_CSS__", "__CINERA_IMAGE__", "__CINERA_JS__", "__CINERA_PROJECT__", "__CINERA_PROJECT_ID__", "__CINERA_SEARCH_URL__", "__CINERA_THEME__", "__CINERA_URL__", }; typedef struct { int Offset; uint32_t AssetIndex; enum8(template_tag_codes) TagCode; } tag_offset; typedef struct { int Validity; // NOTE(matt): Bitmask describing which page the template is valid for, i.e. contents and / or player page int TagCapacity; int TagCount; tag_offset *Tags; } template_metadata; typedef struct { file_buffer File; template_metadata Metadata; } template; // TODO(matt): Consider putting the ref_info and quote_info into linked lists on the heap, just to avoid all the hardcoded sizes typedef struct { char Date[32]; char Text[512]; } quote_info; typedef struct { char Timecode[8]; int Identifier; } identifier; #define MAX_REF_IDENTIFIER_COUNT 64 typedef struct { char RefTitle[620]; char ID[512]; char URL[512]; char Source[256]; identifier Identifier[MAX_REF_IDENTIFIER_COUNT]; int IdentifierCount; } ref_info; typedef struct { char Marker[32]; char WrittenText[32]; } category_info; typedef struct { category_info Category[64]; 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; 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/", 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 { char *Medium; char *Icon; char *WrittenName; } category_medium; category_medium CategoryMedium[] = { // medium icon written name { "admin", "🗹", "Administrivia"}, { "afk", "…" , "Away from Keyboard"}, { "authored", "🗪", "Chat Comment"}, { "blackboard", "🖌", "Blackboard"}, { "drawing", "🎨", "Drawing"}, { "experience", "🍷", "Experience"}, { "hat", "🎩", "Hat"}, { "multimedia", "🎬", "Media Clip"}, { "owl", "🦉", "Owl of Shame"}, { "programming", "🖮", "Programming"}, { "rant", "💢", "Rant"}, { "research", "📖", "Research"}, { "run", "🏃", "In-Game"}, // TODO(matt): Potentially make this written name configurable per project { "speech", "🗩", "Speech"}, { "trivia", "🎲", "Trivia"}, }; enum { NS_CALENDRICAL, NS_LINEAR, NS_SEASONAL, } numbering_schemes; typedef struct { 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[] = { { "bitwise", "Bitwise", "Day", NS_LINEAR, "programming", "" }, { "book", "Book Club", "Day", NS_LINEAR, "research", "" }, { "coad", "Computer Organization and Design", "", NS_LINEAR, "research", "" }, { "reader", "RISC-V Reader", "", NS_LINEAR, "research", "" }, { "riscy", "RISCY BUSINESS", "Day", NS_LINEAR, "programming", "" }, { "risc", "RISCellaneous", "", NS_CALENDRICAL, "speech", "" }, { "chat", "Handmade Chat", "Day", NS_LINEAR, "speech", "" }, { "code", "Handmade Hero", "Day", NS_LINEAR, "programming", "day" }, { "intro-to-c", "Intro to C on Windows", "Day", NS_LINEAR, "programming", "day" }, { "misc", "Handmade Miscellany", "", NS_LINEAR, "admin", "" }, { "ray", "Handmade Ray", "Day", NS_LINEAR, "programming", "" }, { "hmdshow", "HandmadeDev Show", "", NS_SEASONAL, "speech", "ep" }, { "lecture", "Abner Talks", "", NS_SEASONAL, "speech", "" }, { "stream", "Abner Programs", "", NS_SEASONAL, "programming", "" }, { "special", "Abner Show Special", "", NS_SEASONAL, "programming", "" }, { "obbg", "Open Block Building Game", "Episode", NS_LINEAR, "programming", "" }, { "sysadmin", "SysAdmin", "Session", NS_LINEAR, "admin", "" }, }; 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) { for(int i = 0; i < Size; ++i) { String[i] = 0; } } int StringLength(char *String) { int i = 0; while(String[i]) { ++i; } return i; } #define CopyString(Dest, DestSize, Format, ...) CopyString_(__LINE__, (Dest), (DestSize), (Format), ##__VA_ARGS__) __attribute__ ((format (printf, 4, 5))) int CopyString_(int LineNumber, char Dest[], int DestSize, char *Format, ...) { int Length = 0; va_list Args; va_start(Args, Format); Length = vsnprintf(Dest, DestSize, Format, Args); if(Length >= DestSize) { printf("CopyString() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %d(+1)-character string\n", LineNumber, DestSize, Length); __asm__("int3"); } va_end(Args); return Length; } #define CopyStringNoFormat(Dest, DestSize, String) CopyStringNoFormat_(__LINE__, Dest, DestSize, String) int CopyStringNoFormat_(int LineNumber, char *Dest, int DestSize, char *String) { int Length = 0; char *Start = String; while(*String) { *Dest++ = *String++; ++Length; } if(Length >= DestSize) { printf("CopyStringNoFormat() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %d(+1)-character string:\n" "%s\n", LineNumber, DestSize, Length, Start); __asm__("int3"); } *Dest = '\0'; return Length; } #define ClearCopyStringNoFormat(Dest, DestSize, String) ClearCopyStringNoFormat_(__LINE__, Dest, DestSize, String) int ClearCopyStringNoFormat_(int LineNumber, char *Dest, int DestSize, char *String) { Clear(Dest, DestSize); int Length = 0; char *Start = String; while(*String) { *Dest++ = *String++; ++Length; } if(Length >= DestSize) { printf("ClearCopyStringNoFormat() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %d(+1)-character string:\n" "%s\n", LineNumber, DestSize, Length, Start); __asm__("int3"); } *Dest = '\0'; return Length; } // TODO(matt): Maybe do a version of this that takes a string as a Terminator #define CopyStringNoFormatT(Dest, DestSize, String, Terminator) CopyStringNoFormatT_(__LINE__, Dest, DestSize, String, Terminator) int CopyStringNoFormatT_(int LineNumber, char *Dest, int DestSize, char *String, char Terminator) { int Length = 0; char *Start = String; while(*String != Terminator) { *Dest++ = *String++; ++Length; } if(Length >= DestSize) { printf("CopyStringNoFormatT() call on line %d has been passed a buffer too small (%d bytes) to contain %c-terminated %d(+1)-character string:\n" "%.*s\n", LineNumber, DestSize, Terminator == 0 ? '0' : Terminator, Length, Length, Start); __asm__("int3"); } *Dest = '\0'; return Length; } #define CopyStringToBuffer(Dest, Format, ...) CopyStringToBuffer_(__LINE__, Dest, Format, ##__VA_ARGS__) __attribute__ ((format (printf, 3, 4))) void CopyStringToBuffer_(int LineNumber, buffer *Dest, char *Format, ...) { va_list Args; va_start(Args, Format); int Length = vsnprintf(Dest->Ptr, Dest->Size - (Dest->Ptr - Dest->Location), Format, Args); va_end(Args); if(Length + (Dest->Ptr - Dest->Location) >= Dest->Size) { fprintf(stderr, "CopyStringToBuffer(%s) call on line %d cannot accommodate null-terminated %d(+1)-character string:\n" "%s\n", Dest->ID, LineNumber, Length, Format); __asm__("int3"); } Dest->Ptr += Length; } #define CopyStringToBufferNoFormat(Dest, String) CopyStringToBufferNoFormat_(__LINE__, Dest, String) void CopyStringToBufferNoFormat_(int LineNumber, buffer *Dest, char *String) { char *Start = String; while(*String) { *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 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) { char *Start = String; int Length = StringLength(String); while(*String) { switch(*String) { case '<': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'l'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break; case '>': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'g'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break; case '&': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'a'; *Dest->Ptr++ = 'm'; *Dest->Ptr++ = 'p'; *Dest->Ptr++ = ';'; Length += 4; break; case '\"': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'q'; *Dest->Ptr++ = 'u'; *Dest->Ptr++ = 'o'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 5; break; case '\'': *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = '9'; *Dest->Ptr++ = ';'; Length += 4; break; default: *Dest->Ptr++ = *String; break; } ++String; } if(Dest->Ptr - Dest->Location >= Dest->Size) { fprintf(stderr, "CopyStringToBufferHTMLSafe(%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 CopyStringToBufferHTMLSafeBreakingOnSlash(Dest, String) CopyStringToBufferHTMLSafeBreakingOnSlash_(__LINE__, Dest, String) void CopyStringToBufferHTMLSafeBreakingOnSlash_(int LineNumber, buffer *Dest, char *String) { char *Start = String; int Length = StringLength(String); while(*String) { switch(*String) { case '<': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'l'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break; case '>': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'g'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break; case '&': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'a'; *Dest->Ptr++ = 'm'; *Dest->Ptr++ = 'p'; *Dest->Ptr++ = ';'; Length += 4; break; case '\'': *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = '9'; *Dest->Ptr++ = ';'; Length += 4; break; case '\"': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'q'; *Dest->Ptr++ = 'u'; *Dest->Ptr++ = 'o'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 5; break; case '/': *Dest->Ptr++ = '/'; *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '8'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '0'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = ';'; Length += 7; break; default: *Dest->Ptr++ = *String; break; } ++String; } if(Dest->Ptr - Dest->Location >= Dest->Size) { 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) void CopyBuffer_(int LineNumber, buffer *Dest, buffer *Src) { Src->Ptr = Src->Location; while(*Src->Ptr) { *Dest->Ptr++ = *Src->Ptr++; } if(Dest->Ptr - Dest->Location >= Dest->Size) { fprintf(stderr, "CopyBuffer(%s) call on line %d cannot accommodate %d(+1)-character %s\n", Dest->ID, LineNumber, StringLength(Src->Location), Src->ID); __asm__("int3"); } *Dest->Ptr = '\0'; } #define CopyBufferSized(Dest, Src, Size) CopyBufferSized_(__LINE__, Dest, Src, Size) 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) { *Dest->Ptr++ = *Src->Ptr++; } if(Dest->Ptr - Dest->Location >= Dest->Size) { fprintf(stderr, "CopyBufferNoNull(%s) call on line %d cannot accommodate %d(+1)-character %s\n", Dest->ID, LineNumber, StringLength(Src->Location), Src->ID); __asm__("int3"); } } int StringsDiffer(char *A, char *B) // NOTE(matt): Two null-terminated strings { while(*A && *B && *A == *B) { ++A, ++B; } return *A - *B; } int StringsDifferCaseInsensitive(char *A, char *B) // NOTE(matt): Two null-terminated strings { while(*A && *B && ((*A >= 'A' && *A <= 'Z') ? *A + ('a' - 'A') : *A) == ((*B >= 'A' && *B <= 'Z') ? *B + ('a' - 'A') : *B)) { ++A, ++B; } return *A - *B; } bool StringsDifferT(char *A, // NOTE(matt): Null-terminated string char *B, // NOTE(matt): Not null-terminated string (e.g. one mid-buffer) char Terminator // NOTE(matt): Caller definable terminator. Pass 0 to only match on the extent of A ) { // TODO(matt): Make sure this can't crash upon reaching the end of B's buffer int ALength = StringLength(A); int i = 0; while(i < ALength && A[i] && A[i] == B[i]) { ++i; } if((!Terminator && !A[i] && ALength == i) || (!A[i] && ALength == i && (B[i] == Terminator))) { return FALSE; } else { return TRUE; } } 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) { // TODO(matt): Correctly check for permissions int Ancestors = 0; while(mkdir(Path, 00755) == -1) { if(errno == EACCES) { return RC_ERROR_DIRECTORY; } if(StripComponentFromPath(Path) == RC_ERROR_DIRECTORY) { return RC_ERROR_DIRECTORY; } ++Ancestors; } int i = StringLength(Path); while(Ancestors > 0) { while(Path[i] != '\0') { ++i; } Path[i] = '/'; --Ancestors; if((mkdir(Path, 00755)) == -1) { return RC_ERROR_DIRECTORY; } } return RC_SUCCESS; } void LogUsage(buffer *Buffer) { #if DEBUG char LogPath[256]; CopyString(LogPath, "%s/%s", Config.CacheDir, "buffers.log"); FILE *LogFile; if(!(LogFile = fopen(LogPath, "a+"))) { MakeDir(Config.CacheDir); if(!(LogFile = fopen(LogPath, "a+"))) { perror("LogUsage"); return; } } fprintf(LogFile, "%s,%ld,%d\n", Buffer->ID, Buffer->Ptr - Buffer->Location, Buffer->Size); fclose(LogFile); #endif } __attribute__ ((format (printf, 2, 3))) void LogError(int LogLevel, char *Format, ...) { if(Config.LogLevel >= LogLevel) { char LogPath[256]; CopyString(LogPath, sizeof(LogPath), "%s/%s", Config.CacheDir, "errors.log"); FILE *LogFile; if(!(LogFile = fopen(LogPath, "a+"))) { MakeDir(Config.CacheDir); if(!(LogFile = fopen(LogPath, "a+"))) { perror("LogUsage"); return; } } va_list Args; va_start(Args, Format); vfprintf(LogFile, Format, Args); va_end(Args); // TODO(matt): Include the LogLevel "string" and the current wall time fprintf(LogFile, "\n"); fclose(LogFile); } } 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; } void FreeBuffer(buffer *Buffer) { free(Buffer->Location); Buffer->Location = 0; Buffer->Ptr = 0; Buffer->Size = 0; #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Freed %s\n", Buffer->ID); fclose(MemLog); printf(" Freed %s\n", Buffer->ID); #endif Buffer->ID = 0; } int ClaimBuffer(buffer *Buffer, char *ID, int Size) { if(MemoryArena.Ptr - MemoryArena.Location + Size > MemoryArena.Size) { return RC_ARENA_FULL; } Buffer->Location = (char *)MemoryArena.Ptr; Buffer->Size = Size; Buffer->ID = ID; MemoryArena.Ptr += Buffer->Size; *Buffer->Location = '\0'; Buffer->Ptr = Buffer->Location; #if DEBUG float PercentageUsed = (float)(MemoryArena.Ptr - MemoryArena.Location) / MemoryArena.Size * 100; printf(" ClaimBuffer(%s): %d\n" " Total ClaimedMemory: %ld (%.2f%%, leaving %ld free)\n\n", Buffer->ID, Buffer->Size, MemoryArena.Ptr - MemoryArena.Location, PercentageUsed, MemoryArena.Size - (MemoryArena.Ptr - MemoryArena.Location)); #endif return RC_SUCCESS; } void DeclaimBuffer(buffer *Buffer) { *Buffer->Location = '\0'; MemoryArena.Ptr -= Buffer->Size; float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100; #if DEBUG printf("DeclaimBuffer(%s)\n" " Used: %ld / %d (%.2f%%)\n" "\n" " Total ClaimedMemory: %ld\n\n", Buffer->ID, Buffer->Ptr - Buffer->Location, Buffer->Size, PercentageUsed, MemoryArena.Ptr - MemoryArena.Location); #endif LogUsage(Buffer); if(PercentageUsed >= 95.0f) { // 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, "%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 miblodelcarpio@gmail.com LogError(LOG_ERROR, "%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; } void RewindBuffer(buffer *Buffer) { #if DEBUG float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100; printf("Rewinding %s\n" " Used: %ld / %d (%.2f%%)\n\n", Buffer->ID, Buffer->Ptr - Buffer->Location, Buffer->Size, PercentageUsed); #endif Buffer->Ptr = Buffer->Location; } enum { TEMPLATE_SEARCH, TEMPLATE_PLAYER, TEMPLATE_BESPOKE } template_types; char * GetDirectoryPath(char *Filepath) { char *Ptr = Filepath + StringLength(Filepath) - 1; while(Ptr > Filepath && *Ptr != '/') { --Ptr; } if(Ptr == Filepath) { *Ptr++ = '.'; } *Ptr = '\0'; return Filepath; } char * GetBaseFilename(char *Filepath, char *Extension // Including the "." // Pass 0 to retain the whole file path, only without its parent directories ) { char *BaseFilename = Filepath + StringLength(Filepath) - 1; while(BaseFilename > Filepath && *BaseFilename != '/') { --BaseFilename; } if(*BaseFilename == '/') { ++BaseFilename; } BaseFilename[StringLength(BaseFilename) - StringLength(Extension)] = '\0'; return BaseFilename; } void ConstructTemplatePath(template *Template, enum8(template_types) Type) { // NOTE(matt): Bespoke template paths are set relative to: // in Project Edition: ProjectDir // in Single Edition: Parent directory of .hmml file if(Template->File.Path[0] != '/') { char Temp[256]; CopyString(Temp, sizeof(Temp), "%s", Template->File.Path); char *Ptr = Template->File.Path; char *End = Template->File.Path + sizeof(Template->File.Path); if(Type == TEMPLATE_BESPOKE) { if(Config.Edition == EDITION_SINGLE) { Ptr += CopyString(Ptr, End - Ptr, "%s/", GetDirectoryPath(Config.SingleHMMLFilePath)); } else { Ptr += CopyString(Ptr, End - Ptr, "%s/", Config.ProjectDir); } } else { Ptr += CopyString(Ptr, End - Ptr, "%s/", Config.TemplatesDir); } CopyString(Ptr, End - Ptr, "%s", Temp); } } void FitTemplateTag(template *Template) { int BlockSize = 16; if(Template->Metadata.TagCount == Template->Metadata.TagCapacity) { Template->Metadata.TagCapacity += BlockSize; if(Template->Metadata.Tags) { Template->Metadata.Tags = realloc(Template->Metadata.Tags, Template->Metadata.TagCapacity * sizeof(*Template->Metadata.Tags)); } else { Template->Metadata.Tags = calloc(Template->Metadata.TagCapacity, sizeof(*Template->Metadata.Tags)); } } } void PushTemplateTag(template *Template, int Offset, enum8(template_tag_types) TagType, int AssetIndex) { FitTemplateTag(Template); Template->Metadata.Tags[Template->Metadata.TagCount].Offset = Offset; Template->Metadata.Tags[Template->Metadata.TagCount].TagCode = TagType; Template->Metadata.Tags[Template->Metadata.TagCount].AssetIndex = AssetIndex; ++Template->Metadata.TagCount; } void ClearTemplateMetadata(template *Template) { Template->Metadata.TagCapacity = 0; Template->Metadata.TagCount = 0; Template->Metadata.Validity = 0; } void InitTemplate(template *Template, char *Location, enum8(template_types) Type) { CopyStringNoFormat(Template->File.Path, sizeof(Template->File.Path), Location); ConstructTemplatePath(Template, Type); printf("%sPacking%s template: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], Template->File.Path); ReadFileIntoBuffer(&Template->File, 0); ClearTemplateMetadata(Template); } void FreeTemplate(template *Template) { FreeBuffer(&Template->File.Buffer); Clear(Template->File.Path, sizeof(Template->File.Path)); Template->File.FileSize = 0; free(Template->Metadata.Tags); Template->Metadata.Tags = 0; ClearTemplateMetadata(Template); } int TimecodeToSeconds(char *Timecode) { int HMS[3] = { 0, 0, 0 }; // 0 == Seconds; 1 == Minutes; 2 == Hours int Colons = 0; while(*Timecode) { //if((*Timecode < '0' || *Timecode > '9') && *Timecode != ':') { return FALSE; } if(*Timecode == ':') { ++Colons; //if(Colons > 2) { return FALSE; } for(int i = 0; i < Colons; ++i) { HMS[Colons - i] = HMS[Colons - (i + 1)]; } HMS[0] = 0; } else { HMS[0] = HMS[0] * 10 + *Timecode - '0'; } ++Timecode; } //if(HMS[0] > 59 || HMS[1] > 59 || Timecode[-1] == ':') { return FALSE; } return HMS[2] * 60 * 60 + HMS[1] * 60 + HMS[0]; } typedef struct { unsigned int Hue:16; unsigned int Saturation:8; unsigned int Lightness:8; } hsl_colour; hsl_colour CharToColour(char Char) { hsl_colour Colour; if(Char >= 'a' && Char <= 'z') { Colour.Hue = (((float)Char - 'a') / ('z' - 'a') * 360); Colour.Saturation = (((float)Char - 'a') / ('z' - 'a') * 26 + 74); } else if(Char >= 'A' && Char <= 'Z') { Colour.Hue = (((float)Char - 'A') / ('Z' - 'A') * 360); Colour.Saturation = (((float)Char - 'A') / ('Z' - 'A') * 26 + 74); } else if(Char >= '0' && Char <= '9') { Colour.Hue = (((float)Char - '0') / ('9' - '0') * 360); Colour.Saturation = (((float)Char - '0') / ('9' - '0') * 26 + 74); } else { Colour.Hue = 180; Colour.Saturation = 50; } return Colour; } void StringToColourHash(hsl_colour *Colour, char *String) { Colour->Hue = 0; Colour->Saturation = 0; Colour->Lightness = 74; int i; for(i = 0; String[i]; ++i) { Colour->Hue += CharToColour(String[i]).Hue; Colour->Saturation += CharToColour(String[i]).Saturation; } Colour->Hue = Colour->Hue % 360; Colour->Saturation = Colour->Saturation % 26 + 74; } char * SanitisePunctuation(char *String) { char *Ptr = String; while(*Ptr) { if(*Ptr == ' ') { *Ptr = '_'; } if((*Ptr < '0' || *Ptr > '9') && (*Ptr < 'a' || *Ptr > 'z') && (*Ptr < 'A' || *Ptr > 'Z')) { *Ptr = '-'; } ++Ptr; } return String; } enum { PAGE_PLAYER = 1 << 0, PAGE_SEARCH = 1 << 1 } pages; void ConstructURLPrefix(buffer *URLPrefix, enum8(asset_types) AssetType, enum8(pages) PageType) { RewindBuffer(URLPrefix); if(StringsDiffer(Config.RootURL, "")) { CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.RootURL); CopyStringToBuffer(URLPrefix, "/"); } else { if(Config.Edition == EDITION_PROJECT) { if(PageType == PAGE_PLAYER) { CopyStringToBuffer(URLPrefix, "../"); } CopyStringToBuffer(URLPrefix, "../"); } } switch(AssetType) { case ASSET_CSS: if(StringsDiffer(Config.CSSDir, "")) { CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.CSSDir); CopyStringToBuffer(URLPrefix, "/"); } break; case ASSET_IMG: if(StringsDiffer(Config.ImagesDir, "")) { CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.ImagesDir); CopyStringToBuffer(URLPrefix, "/"); } break; case ASSET_JS: if(StringsDiffer(Config.JSDir, "")) { CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.JSDir); CopyStringToBuffer(URLPrefix, "/"); } break; } } typedef struct { char Abbreviation[32]; hsl_colour Colour; credential_info *Credential; bool Seen; } speaker; typedef struct { speaker Speaker[16]; int Count; } speakers; enum { CreditsError_NoHost, CreditsError_NoAnnotator, CreditsError_NoCredentials } credits_errors; 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_MOVED_TO | 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) { bool Found = FALSE; for(int CredentialIndex = 0; CredentialIndex < ArrayCount(Credentials); ++CredentialIndex) { if(!StringsDiffer(Person, Credentials[CredentialIndex].Username)) { if(Speakers) { Speakers->Speaker[Speakers->Count].Credential = &Credentials[CredentialIndex]; ++Speakers->Count; } Found = TRUE; if(*HasCreditsMenu == FALSE) { CopyStringToBuffer(CreditsMenu, "
\n" " Credits\n" "
\n"); *HasCreditsMenu = TRUE; } CopyStringToBuffer(CreditsMenu, " \n"); if(Credentials[CredentialIndex].HomepageURL) { CopyStringToBuffer(CreditsMenu, " \n" "
%s
\n" "
%s
\n" "
\n", Credentials[CredentialIndex].HomepageURL, Role, Credentials[CredentialIndex].CreditedName); } else { CopyStringToBuffer(CreditsMenu, "
\n" "
%s
\n" "
%s
\n" "
\n", Role, Credentials[CredentialIndex].CreditedName); } if(Credentials[CredentialIndex].SupportURL) { buffer URL; ConstructResolvedAssetURL(&URL, Credentials[CredentialIndex].SupportIconIndex, PAGE_PLAYER); CopyStringToBuffer(CreditsMenu, "
\n"); } CopyStringToBuffer(CreditsMenu, "
\n"); } } return Found ? RC_SUCCESS : CreditsError_NoCredentials; } void ClearNullTerminatedString(char *String) { while(*String) { *String++ = '\0'; } } void InitialString(char *Dest, char *Src) { ClearNullTerminatedString(Dest); *Dest++ = *Src++; while(*Src++) { if(*Src == ' ') { ++Src; if(*Src) { *Dest++ = *Src; } } } } void GetFirstSubstring(char *Dest, char *Src) { ClearNullTerminatedString(Dest); while(*Src && *Src != ' ') { *Dest++ = *Src++; } } void InitialAndGetFinalString(char *Dest, int DestSize, char *Src) { ClearNullTerminatedString(Dest); int SrcLength = StringLength(Src); char *SrcPtr = Src + SrcLength - 1; while(SrcPtr > Src && *SrcPtr != ' ') { --SrcPtr; } if(*SrcPtr == ' ' && SrcPtr - Src < SrcLength - 1) { ++SrcPtr; } if(Src < SrcPtr) { *Dest++ = *Src++; *Dest++ = '.'; *Dest++ = ' '; while(Src < SrcPtr - 1) { if(*Src == ' ') { ++Src; if(*Src) { *Dest++ = *Src; *Dest++ = '.'; *Dest++ = ' '; } } ++Src; } } CopyString(Dest, DestSize, "%s", SrcPtr); } bool AbbreviationsClash(speakers *Speakers) { for(int i = 0; i < Speakers->Count; ++i) { for(int j = i + 1; j < Speakers->Count; ++j) { if(!StringsDiffer(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[j].Abbreviation)) { return TRUE; } } } return FALSE; } void SortAndAbbreviateSpeakers(speakers *Speakers) { for(int i = 0; i < Speakers->Count; ++i) { for(int j = i + 1; j < Speakers->Count; ++j) { if(StringsDiffer(Speakers->Speaker[i].Credential->Username, Speakers->Speaker[j].Credential->Username) > 0) { credential_info *Temp = Speakers->Speaker[j].Credential; Speakers->Speaker[j].Credential = Speakers->Speaker[i].Credential; Speakers->Speaker[i].Credential = Temp; break; } } } for(int i = 0; i < Speakers->Count; ++i) { StringToColourHash(&Speakers->Speaker[i].Colour, Speakers->Speaker[i].Credential->Username); InitialString(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Credential->CreditedName); } int Attempt = 0; while(AbbreviationsClash(Speakers)) { for(int i = 0; i < Speakers->Count; ++i) { switch(Attempt) { case 0: GetFirstSubstring(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Credential->CreditedName); break; case 1: InitialAndGetFinalString(Speakers->Speaker[i].Abbreviation, sizeof(Speakers->Speaker[i].Abbreviation), Speakers->Speaker[i].Credential->CreditedName); break; case 2: ClearCopyStringNoFormat(Speakers->Speaker[i].Abbreviation, sizeof(Speakers->Speaker[i].Abbreviation), Speakers->Speaker[i].Credential->Username); break; } } ++Attempt; } } int BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Metadata, speakers *Speakers) // TODO(matt): Make this take the Credentials, once we are parsing them from a config { if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->member, "Host", Speakers) == CreditsError_NoCredentials) { 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); return CreditsError_NoCredentials; } if(Metadata->co_host_count > 0) { for(int i = 0; i < Metadata->co_host_count; ++i) { if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->co_hosts[i], "Co-host", Speakers) == CreditsError_NoCredentials) { 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]); return CreditsError_NoCredentials; } } } if(Metadata->guest_count > 0) { for(int i = 0; i < Metadata->guest_count; ++i) { if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->guests[i], "Guest", Speakers) == CreditsError_NoCredentials) { 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]); return CreditsError_NoCredentials; } } } if(Speakers->Count > 1) { SortAndAbbreviateSpeakers(Speakers); } if(Metadata->annotator_count > 0) { for(int i = 0; i < Metadata->annotator_count; ++i) { if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->annotators[i], "Annotator", 0) == CreditsError_NoCredentials) { 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]); return CreditsError_NoCredentials; } } } else { if(*HasCreditsMenu == TRUE) { CopyStringToBuffer(CreditsMenu, "
\n" "
\n"); } fprintf(stderr, "Missing \"annotator\" in the [video] node\n"); return CreditsError_NoAnnotator; } if(*HasCreditsMenu == TRUE) { CopyStringToBuffer(CreditsMenu, " \n" " \n"); } return RC_SUCCESS; } enum { REF_SITE = 1 << 0, REF_PAGE = 1 << 1, REF_URL = 1 << 2, REF_TITLE = 1 << 3, REF_ARTICLE = 1 << 4, REF_AUTHOR = 1 << 5, REF_EDITOR = 1 << 6, REF_PUBLISHER = 1 << 7, REF_ISBN = 1 << 8, } reference_fields; int BuildReference(ref_info *ReferencesArray, int RefIdentifier, int UniqueRefs, HMML_Reference *Ref, HMML_Annotation *Anno) { if(Ref->isbn) { CopyString(ReferencesArray[UniqueRefs].ID, sizeof(ReferencesArray[UniqueRefs].ID), "%s", Ref->isbn); if(!Ref->url) { CopyString(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), "https://isbndb.com/book/%s", Ref->isbn); } else { CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), Ref->url); } } else if(Ref->url) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, sizeof(ReferencesArray[UniqueRefs].ID), Ref->url); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), Ref->url); } else { return RC_INVALID_REFERENCE; } int Mask = 0; if(Ref->site) { Mask |= REF_SITE; } if(Ref->page) { Mask |= REF_PAGE; } if(Ref->title) { Mask |= REF_TITLE; } if(Ref->article) { Mask |= REF_ARTICLE; } if(Ref->author) { Mask |= REF_AUTHOR; } if(Ref->editor) { Mask |= REF_EDITOR; } if(Ref->publisher) { Mask |= REF_PUBLISHER; } // TODO(matt): Consider handling the various combinations more flexibly, unless we defer this stuff until we have the // reference store, in which we could optionally customise the display of each reference entry switch(Mask) { case (REF_TITLE | REF_AUTHOR | REF_PUBLISHER): { CopyString(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), "%s (%s)", Ref->author, Ref->publisher); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title); } break; case (REF_AUTHOR | REF_SITE | REF_PAGE): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->site); CopyString(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), "%s: \"%s\"", Ref->author, Ref->page); } break; case (REF_PAGE | REF_TITLE): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->title); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->page); } break; case (REF_SITE | REF_PAGE): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->site); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->page); } break; case (REF_SITE | REF_TITLE): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->site); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title); } break; case (REF_TITLE | REF_AUTHOR): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->author); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title); } break; case (REF_ARTICLE | REF_AUTHOR): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->author); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->article); } break; case (REF_TITLE | REF_PUBLISHER): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->publisher); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title); } break; case REF_TITLE: { CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title); } break; case REF_SITE: { CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->site); } break; default: return RC_INVALID_REFERENCE; break; } CopyString(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode, sizeof(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode), "%s", Anno->time); ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Identifier = RefIdentifier; return RC_SUCCESS; } void InsertCategory(categories *GlobalTopics, categories *LocalTopics, categories *GlobalMedia, categories *LocalMedia, char *Marker) { bool IsMedium = FALSE; int CategoryMediumIndex; for(CategoryMediumIndex = 0; CategoryMediumIndex < ArrayCount(CategoryMedium); ++CategoryMediumIndex) { if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, Marker)) { IsMedium = TRUE; break; } } if(IsMedium) { int MediumIndex; for(MediumIndex = 0; MediumIndex < LocalMedia->Count; ++MediumIndex) { if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, LocalMedia->Category[MediumIndex].Marker)) { return; } if((StringsDiffer(CategoryMedium[CategoryMediumIndex].WrittenName, LocalMedia->Category[MediumIndex].WrittenText)) < 0) { int CategoryCount; for(CategoryCount = LocalMedia->Count; CategoryCount > MediumIndex; --CategoryCount) { CopyString(LocalMedia->Category[CategoryCount].Marker, sizeof(LocalMedia->Category[CategoryCount].Marker), "%s", LocalMedia->Category[CategoryCount-1].Marker); CopyString(LocalMedia->Category[CategoryCount].WrittenText, sizeof(LocalMedia->Category[CategoryCount].WrittenText), "%s", LocalMedia->Category[CategoryCount-1].WrittenText); } CopyString(LocalMedia->Category[CategoryCount].Marker, sizeof(LocalMedia->Category[CategoryCount].Marker), "%s", CategoryMedium[CategoryMediumIndex].Medium); CopyString(LocalMedia->Category[CategoryCount].WrittenText, sizeof(LocalMedia->Category[CategoryCount].WrittenText), "%s", CategoryMedium[CategoryMediumIndex].WrittenName); break; } } if(MediumIndex == LocalMedia->Count) { CopyString(LocalMedia->Category[MediumIndex].Marker, sizeof(LocalMedia->Category[MediumIndex].Marker), "%s", CategoryMedium[CategoryMediumIndex].Medium); CopyString(LocalMedia->Category[MediumIndex].WrittenText, sizeof(LocalMedia->Category[MediumIndex].WrittenText), "%s", CategoryMedium[CategoryMediumIndex].WrittenName); } ++LocalMedia->Count; for(MediumIndex = 0; MediumIndex < GlobalMedia->Count; ++MediumIndex) { if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, GlobalMedia->Category[MediumIndex].Marker)) { return; } if((StringsDiffer(CategoryMedium[CategoryMediumIndex].WrittenName, GlobalMedia->Category[MediumIndex].WrittenText)) < 0) { int CategoryCount; for(CategoryCount = GlobalMedia->Count; CategoryCount > MediumIndex; --CategoryCount) { CopyString(GlobalMedia->Category[CategoryCount].Marker, sizeof(GlobalMedia->Category[CategoryCount].Marker), "%s", GlobalMedia->Category[CategoryCount-1].Marker); CopyString(GlobalMedia->Category[CategoryCount].WrittenText, sizeof(GlobalMedia->Category[CategoryCount].WrittenText), "%s", GlobalMedia->Category[CategoryCount-1].WrittenText); } CopyString(GlobalMedia->Category[CategoryCount].Marker, sizeof(GlobalMedia->Category[CategoryCount].Marker), "%s", CategoryMedium[CategoryMediumIndex].Medium); CopyString(GlobalMedia->Category[CategoryCount].WrittenText, sizeof(GlobalMedia->Category[CategoryCount].WrittenText), "%s", CategoryMedium[CategoryMediumIndex].WrittenName); break; } } if(MediumIndex == GlobalMedia->Count) { CopyString(GlobalMedia->Category[MediumIndex].Marker, sizeof(GlobalMedia->Category[MediumIndex].Marker), "%s", CategoryMedium[CategoryMediumIndex].Medium); CopyString(GlobalMedia->Category[MediumIndex].WrittenText, sizeof(GlobalMedia->Category[MediumIndex].WrittenText), "%s", CategoryMedium[CategoryMediumIndex].WrittenName); } ++GlobalMedia->Count; } else { int TopicIndex; for(TopicIndex = 0; TopicIndex < LocalTopics->Count; ++TopicIndex) { if(!StringsDiffer(Marker, LocalTopics->Category[TopicIndex].Marker)) { return; } if((StringsDiffer(Marker, LocalTopics->Category[TopicIndex].Marker)) < 0) { int CategoryCount; for(CategoryCount = LocalTopics->Count; CategoryCount > TopicIndex; --CategoryCount) { CopyString(LocalTopics->Category[CategoryCount].Marker, sizeof(LocalTopics->Category[CategoryCount].Marker), "%s", LocalTopics->Category[CategoryCount-1].Marker); } CopyString(LocalTopics->Category[CategoryCount].Marker, sizeof(LocalTopics->Category[CategoryCount].Marker), "%s", Marker); break; } } if(TopicIndex == LocalTopics->Count) { CopyString(LocalTopics->Category[TopicIndex].Marker, sizeof(LocalTopics->Category[TopicIndex].Marker), "%s", Marker); } ++LocalTopics->Count; for(TopicIndex = 0; TopicIndex < GlobalTopics->Count; ++TopicIndex) { if(!StringsDiffer(Marker, GlobalTopics->Category[TopicIndex].Marker)) { return; } // NOTE(matt): This successfully sorts "nullTopic" at the end, but maybe figure out a more general way to force the // order of stuff, perhaps blocks of dudes that should sort to the start / end if(((StringsDiffer(Marker, GlobalTopics->Category[TopicIndex].Marker)) < 0 || !StringsDiffer(GlobalTopics->Category[TopicIndex].Marker, "nullTopic"))) { if(StringsDiffer(Marker, "nullTopic")) // NOTE(matt): This test (with the above || condition) forces nullTopic never to be inserted, only appended { int CategoryCount; for(CategoryCount = GlobalTopics->Count; CategoryCount > TopicIndex; --CategoryCount) { CopyString(GlobalTopics->Category[CategoryCount].Marker, sizeof(GlobalTopics->Category[CategoryCount].Marker), "%s", GlobalTopics->Category[CategoryCount-1].Marker); } CopyString(GlobalTopics->Category[CategoryCount].Marker, sizeof(GlobalTopics->Category[CategoryCount].Marker), "%s", Marker); break; } } } if(TopicIndex == GlobalTopics->Count) { CopyString(GlobalTopics->Category[TopicIndex].Marker, sizeof(GlobalTopics->Category[TopicIndex].Marker), "%s", Marker); } ++GlobalTopics->Count; } } void BuildCategories(buffer *AnnotationClass, buffer *CategoryIcons, categories *LocalTopics, categories *LocalMedia, int *MarkerIndex, char *DefaultMedium) { bool CategoriesSpan = FALSE; if(!(LocalTopics->Count == 1 && !StringsDiffer(LocalTopics->Category[0].Marker, "nullTopic") && LocalMedia->Count == 1 && !StringsDiffer(LocalMedia->Category[0].Marker, DefaultMedium))) { CategoriesSpan = TRUE; CopyStringToBuffer(CategoryIcons, ""); } if(LocalTopics->Count == 1 && !StringsDiffer(LocalTopics->Category[0].Marker, "nullTopic")) { char SanitisedMarker[StringLength(LocalTopics->Category[0].Marker) + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalTopics->Category[0].Marker); SanitisePunctuation(SanitisedMarker); CopyStringToBuffer(AnnotationClass, " cat_%s", SanitisedMarker); } else { for(int i = 0; i < LocalTopics->Count; ++i) { char SanitisedMarker[StringLength(LocalTopics->Category[i].Marker) + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalTopics->Category[i].Marker); SanitisePunctuation(SanitisedMarker); CopyStringToBuffer(CategoryIcons, "
", LocalTopics->Category[i].Marker, SanitisedMarker); CopyStringToBuffer(AnnotationClass, " cat_%s", SanitisedMarker); } } if(LocalMedia->Count == 1 && !StringsDiffer(LocalMedia->Category[0].Marker, DefaultMedium)) { char SanitisedMarker[StringLength(LocalMedia->Category[0].Marker) + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalMedia->Category[0].Marker); SanitisePunctuation(SanitisedMarker); CopyStringToBuffer(AnnotationClass, " %s", SanitisedMarker); } else { for(int i = 0; i < LocalMedia->Count; ++i) { char SanitisedMarker[StringLength(LocalMedia->Category[i].Marker) + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalMedia->Category[i].Marker); SanitisePunctuation(SanitisedMarker); if(!StringsDiffer(LocalMedia->Category[i].Marker, "afk")) // TODO(matt): Initially hidden config { CopyStringToBuffer(AnnotationClass, " off_%s skip", SanitisedMarker); // TODO(matt): Bulletproof this? } else { for(int j = 0; j < ArrayCount(CategoryMedium); ++j) { if(!StringsDiffer(LocalMedia->Category[i].Marker, CategoryMedium[j].Medium)) { CopyStringToBuffer(CategoryIcons, "
%s
", LocalMedia->Category[i].WrittenText, LocalMedia->Category[i].Marker, CategoryMedium[j].Icon); CopyStringToBuffer(AnnotationClass, " %s", SanitisedMarker); } } } } } if(CategoriesSpan) { CopyStringToBuffer(CategoryIcons, "
"); } CopyStringToBuffer(AnnotationClass, "\""); } int StringToInt(char *String) { int Result = 0; while(*String) { Result = Result * 10 + (*String - '0'); ++String; } return Result; } size_t CurlIntoBuffer(char *InPtr, size_t CharLength, size_t Chars, char **OutputPtr) { int Length = CharLength * Chars; int i; for(i = 0; InPtr[i] && i < Length; ++i) { *((*OutputPtr)++) = InPtr[i]; } **OutputPtr = '\0'; return Length; }; void CurlQuotes(buffer *QuoteStaging, char *QuotesURL) { fprintf(stderr, "%sFetching%s quotes: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], QuotesURL); CURL *curl = curl_easy_init(); if(curl) { CURLcode CurlReturnCode; curl_easy_setopt(curl, CURLOPT_WRITEDATA, &QuoteStaging->Ptr); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer); curl_easy_setopt(curl, CURLOPT_URL, QuotesURL); if((CurlReturnCode = curl_easy_perform(curl))) { fprintf(stderr, "%s\n", curl_easy_strerror(CurlReturnCode)); } curl_easy_cleanup(curl); } } int SearchQuotes(buffer *QuoteStaging, int CacheSize, quote_info *Info, int ID) { QuoteStaging->Ptr = QuoteStaging->Location; while(QuoteStaging->Ptr - QuoteStaging->Location < CacheSize) { char InID[8] = { 0 }; char InTime[16] = { 0 }; QuoteStaging->Ptr += CopyStringNoFormatT(InID, sizeof(InID), QuoteStaging->Ptr, ',') + 1; // Skip past the ',' if(StringToInt(InID) == ID) { QuoteStaging->Ptr += CopyStringNoFormatT(InTime, sizeof(InTime), QuoteStaging->Ptr, ',') + 1; // Skip past the ',' long int Time = StringToInt(InTime); char DayString[3] = { 0 }; strftime(DayString, 3, "%d", gmtime(&Time)); int Day = StringToInt(DayString); char DaySuffix[3]; if(DayString[1] == '1' && Day != 11) { CopyString(DaySuffix, sizeof(DaySuffix), "st"); } else if(DayString[1] == '2' && Day != 12) { CopyString(DaySuffix, sizeof(DaySuffix), "nd"); } else if(DayString[1] == '3' && Day != 13) { CopyString(DaySuffix, sizeof(DaySuffix), "rd"); } else { CopyString(DaySuffix, sizeof(DaySuffix), "th"); } char MonthYear[32]; strftime(MonthYear, 32, "%B, %Y", gmtime(&Time)); CopyString(Info->Date, sizeof(Info->Date), "%d%s %s", Day, DaySuffix, MonthYear); CopyStringNoFormatT(Info->Text, sizeof(Info->Text), QuoteStaging->Ptr, '\n'); FreeBuffer(QuoteStaging); return RC_FOUND; } else { while(*QuoteStaging->Ptr != '\n') { ++QuoteStaging->Ptr; } ++QuoteStaging->Ptr; } } return RC_UNFOUND; } int BuildQuote(quote_info *Info, char *Speaker, int ID, bool ShouldFetchQuotes) { char QuoteCacheDir[256]; CopyString(QuoteCacheDir, sizeof(QuoteCacheDir), "%s/quotes", Config.CacheDir); char QuoteCachePath[256]; CopyString(QuoteCachePath, sizeof(QuoteCachePath), "%s/%s", QuoteCacheDir, Speaker); FILE *QuoteCache; char QuotesURL[256]; // TODO(matt): Make the URL configurable and also handle the case in which the .raw isn't available CopyString(QuotesURL, sizeof(QuotesURL), "https://dev.abaines.me.uk/quotes/%s.raw", Speaker); bool CacheAvailable = FALSE; if(!(QuoteCache = fopen(QuoteCachePath, "a+"))) { if(MakeDir(QuoteCacheDir) == RC_SUCCESS) { CacheAvailable = TRUE; } if(!(QuoteCache = fopen(QuoteCachePath, "a+"))) { fprintf(stderr, "Unable to open quote cache %s: %s\n", QuoteCachePath, strerror(errno)); } else { CacheAvailable = TRUE; } } else { CacheAvailable = TRUE; } buffer QuoteStaging; QuoteStaging.ID = "QuoteStaging"; QuoteStaging.Size = Kilobytes(256); if(!(QuoteStaging.Location = malloc(QuoteStaging.Size))) { fclose(QuoteCache); return RC_ERROR_MEMORY; } #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Allocated QuoteStaging (%d)\n", QuoteStaging.Size); fclose(MemLog); printf(" Allocated QuoteStaging (%d)\n", QuoteStaging.Size); #endif QuoteStaging.Ptr = QuoteStaging.Location; if(CacheAvailable) { fseek(QuoteCache, 0, SEEK_END); int FileSize = ftell(QuoteCache); fseek(QuoteCache, 0, SEEK_SET); fread(QuoteStaging.Location, FileSize, 1, QuoteCache); fclose(QuoteCache); if(ShouldFetchQuotes || SearchQuotes(&QuoteStaging, FileSize, Info, ID) == RC_UNFOUND) { // TODO(matt): Error handling CurlQuotes(&QuoteStaging, QuotesURL); if(!(QuoteCache = fopen(QuoteCachePath, "w"))) { perror(QuoteCachePath); } fwrite(QuoteStaging.Location, QuoteStaging.Ptr - QuoteStaging.Location, 1, QuoteCache); fclose(QuoteCache); int CacheSize = QuoteStaging.Ptr - QuoteStaging.Location; QuoteStaging.Ptr = QuoteStaging.Location; if(SearchQuotes(&QuoteStaging, CacheSize, Info, ID) == RC_UNFOUND) { FreeBuffer(&QuoteStaging); return RC_UNFOUND; } } } else { CurlQuotes(&QuoteStaging, QuotesURL); int CacheSize = QuoteStaging.Ptr - QuoteStaging.Location; QuoteStaging.Ptr = QuoteStaging.Location; if(SearchQuotes(&QuoteStaging, CacheSize, Info, ID) == RC_UNFOUND) { FreeBuffer(&QuoteStaging); return RC_UNFOUND; } } return RC_SUCCESS; } int GenerateTopicColours(char *Topic) { char SanitisedTopic[StringLength(Topic) + 1]; CopyString(SanitisedTopic, sizeof(SanitisedTopic), "%s", Topic); SanitisePunctuation(SanitisedTopic); for(int i = 0; i < ArrayCount(CategoryMedium); ++i) { if(!StringsDiffer(Topic, CategoryMedium[i].Medium)) { return RC_NOOP; } } file_buffer Topics; Topics.Buffer.ID = "Topics"; if(StringsDiffer(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/%s", Config.RootDir, BuiltinAssets[ASSET_CSS_TOPICS].Filename); } char *Ptr = Topics.Path + StringLength(Topics.Path) - 1; while(*Ptr != '/') { --Ptr; } *Ptr = '\0'; DIR *CSSDirHandle; // TODO(matt): open() if(!(CSSDirHandle = opendir(Topics.Path))) { if(MakeDir(Topics.Path) == RC_ERROR_DIRECTORY) { LogError(LOG_ERROR, "Unable to create directory %s: %s", Topics.Path, strerror(errno)); fprintf(stderr, "Unable to create directory %s: %s\n", Topics.Path, strerror(errno)); return RC_ERROR_DIRECTORY; }; } closedir(CSSDirHandle); *Ptr = '/'; if((Topics.Handle = fopen(Topics.Path, "a+"))) { fseek(Topics.Handle, 0, SEEK_END); Topics.FileSize = ftell(Topics.Handle); Topics.Buffer.Size = Topics.FileSize; fseek(Topics.Handle, 0, SEEK_SET); if(!(Topics.Buffer.Location = malloc(Topics.Buffer.Size))) { return RC_ERROR_MEMORY; } #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Allocated Topics (%d)\n", Topics.Buffer.Size); fclose(MemLog); printf(" Allocated Topics (%d)\n", Topics.Buffer.Size); #endif Topics.Buffer.Ptr = Topics.Buffer.Location; fread(Topics.Buffer.Location, Topics.Buffer.Size, 1, Topics.Handle); while(Topics.Buffer.Ptr - Topics.Buffer.Location < Topics.Buffer.Size) { Topics.Buffer.Ptr += StringLength(".category."); if(!StringsDifferT(SanitisedTopic, Topics.Buffer.Ptr, ' ')) { FreeBuffer(&Topics.Buffer); fclose(Topics.Handle); return RC_NOOP; } while(Topics.Buffer.Ptr - Topics.Buffer.Location < Topics.Buffer.Size && *Topics.Buffer.Ptr != '\n') { ++Topics.Buffer.Ptr; } ++Topics.Buffer.Ptr; } if(!StringsDiffer(Topic, "nullTopic")) { fprintf(Topics.Handle, ".category.%s { border: 1px solid transparent; background: transparent; }\n", SanitisedTopic); } else { hsl_colour Colour; StringToColourHash(&Colour, Topic); fprintf(Topics.Handle, ".category.%s { border: 1px solid hsl(%d, %d%%, %d%%); background: hsl(%d, %d%%, %d%%); }\n", SanitisedTopic, Colour.Hue, Colour.Saturation, Colour.Lightness, Colour.Hue, Colour.Saturation, Colour.Lightness); } 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 actual dir... perror(Topics.Path); return RC_ERROR_FILE; } } void PrintUsage(char *BinaryLocation, config *DefaultConfig) { fprintf(stderr, "Usage: %s [option(s)] filename(s)\n" "\n" "Options:\n" " Paths: %s(advisedly universal, but may be set per-(sub)project as required)%s\n" " -r \n" " Override default assets root directory (\"%s\")\n" " -R \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 \n" " Override default CSS directory (\"%s\"), relative to root\n" " -i \n" " Override default images directory (\"%s\"), relative to root\n" " -j \n" " Override default JS directory (\"%s\"), relative to root\n" " -Q \n" " Override default query string (\"%s\")\n" " To disable revved resources, set an empty string with -Q \"\"\n" "\n" " Project Settings:\n" " -p \n" " Set the project ID, triggering PROJECT EDITION\n" " -m \n" " Override default default medium (\"%s\")\n" " %sKnown project defaults:\n", BinaryLocation, 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, ColourStrings[CS_COMMENT]); for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex) { fprintf(stderr, " %s:", ProjectInfo[ProjectIndex].ProjectID); // NOTE(matt): This kind of thing really needs to loop over the dudes once to find the longest one for(int i = 11; i > StringLength(ProjectInfo[ProjectIndex].ProjectID); i -= 4) { fprintf(stderr, "\t"); } fprintf(stderr, "%s\n", ProjectInfo[ProjectIndex].Medium); } fprintf(stderr, "%s -s