#if 0 if [ $(command -v ctime 2>/dev/null) ]; then ctime -begin ${0%.*}.ctm fi #gcc -g -fsanitize=address -Wall -std=c99 -pipe $0 -o ${0%.*} -lcurl gcc -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} -lcurl #clang -fsanitize=address -g -Wall -std=c99 -pipe $0 -o ${0%.*} -lcurl #clang -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} -lcurl if [ $(command -v ctime 2>/dev/null) ]; then ctime -end ${0%.*}.ctm fi exit #endif #include typedef struct { uint32_t Major, Minor, Patch; } version; version CINERA_APP_VERSION = { .Major = 0, .Minor = 10, .Patch = 30 }; #define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW #include // NOTE(matt): open() #undef __USE_XOPEN2K8 #include // NOTE(matt): varargs #define __USE_POSIX // NOTE(matt): fileno() #include // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror #undef __USE_POSIX #include // NOTE(matt): calloc, malloc, free #include // NOTE(matt): getopts #include #include #include // NOTE(matt): flock #include #include #include #include // NOTE(matt): strerror #include //NOTE(matt): errno #include // NOTE(matt): inotify #include // NOTE(matt): ioctl and TIOCGWINSZ #include #define __USE_XOPEN2K // NOTE(matt): readlink() #include // NOTE(matt): sleep() #undef __USE_XOPEN2K #define __USE_POSIX #include // NOTE(matt): sigaction() and sigemptyset() #undef __USE_POSIX #define HMMLIB_IMPLEMENTATION #include "hmmlib.h" #define STB_IMAGE_IMPLEMENTATION #define STBI_NO_LINEAR #define STBI_NO_HDR #define STBI_ONLY_GIF #define STBI_ONLY_JPEG #define STBI_ONLY_PNG #include "stb_image.h" typedef uint64_t bool; #define TRUE 1 #define FALSE 0 #define NA 0 #define SECONDS_PER_HOUR 3600 #define SECONDS_PER_MINUTE 60 #define enum8(type) int8_t #define enum16(type) int16_t #define enum32(type) int32_t // DEBUG SWITCHES #define DEBUG_MEMORY_LEAKAGE 0 #define DEBUG_MEMORY_LEAKAGE_LOOPED 0 #define DEBUG_PRINT_FUNCTION_NAMES 0 #define DEBUG_PROJECT_INDICES 0 #define DEBUG_CLASH_RESOLVER 0 #define DEBUG 0 #define DEBUG_MEM 0 // //// typedef struct { char *Base; uint64_t Length; } string; typedef struct { char *Base; char *Ptr; } memory_page; typedef struct memory_pen_location { memory_page *Page; string String; struct memory_pen_location *Prev; struct memory_pen_location *Next; } memory_pen_location; // TODO(matt): We probably want our memory_book to be able to support strings that span multiple pages // SearchQuotes() especially could benefit from this typedef struct { int64_t PageCount; uint64_t PageSize; uint64_t DataWidthInBytes; uint64_t ItemCount; memory_page *Pages; memory_pen_location *Pen; } memory_book; #define _memory_book(type) memory_book typedef struct { memory_book Message[2]; memory_book *DesiredMessage; memory_book *LastMessage; uint64_t RepetitionCount; bool LastMessageMayBeDeduplicated; } message_control; #define SwapPtrs(A, B) void *Temp = A; A = B; B = Temp; message_control MESSAGE_CONTROL; #define Print(...) fprintf(__VA_ARGS__); MESSAGE_CONTROL.LastMessageMayBeDeduplicated = FALSE #define WriteToFile(...) fprintf(__VA_ARGS__) bool PROFILING = 0; clock_t TIMING_START; #define START_TIMING_BLOCK(...) if(PROFILING) { Print(stdout, __VA_ARGS__); TIMING_START = clock(); } #define END_TIMING_BLOCK() if(PROFILING) { Print(stdout, "\e[1;34m%ld\e[0m\n", clock() - TIMING_START);} #define Kilobytes(Bytes) Bytes << 10 #define Megabytes(Bytes) Bytes << 20 #define MAX_UNIT_LENGTH 16 #define MAX_PROJECT_ID_LENGTH 32 #define MAX_THEME_LENGTH MAX_PROJECT_ID_LENGTH #define MAX_PROJECT_NAME_LENGTH 64 #define MAX_BASE_DIR_LENGTH 128 #define MAX_BASE_URL_LENGTH 128 #define MAX_RELATIVE_PAGE_LOCATION_LENGTH 32 #define MAX_VOD_ID_LENGTH 256 // TODO(matt): This was 32, upped to 256 to fit full paths of "raw" videos. // It hasn't mattered, but will do when the database contains everything. #define MAX_ROOT_DIR_LENGTH 128 #define MAX_ROOT_URL_LENGTH 128 #define MAX_RELATIVE_ASSET_LOCATION_LENGTH 32 #define MAX_BASE_FILENAME_LENGTH 32 #define MAX_ENTRY_OUTPUT_LENGTH MAX_BASE_FILENAME_LENGTH #define MAX_TITLE_LENGTH 128 #define MAX_NUMBER_LENGTH 16 #define MAX_ASSET_FILENAME_LENGTH 64 // TODO(matt): Stop distinguishing between short / long and lift the size limit once we're on the LUT #define MAX_CUSTOM_SNIPPET_SHORT_LENGTH 256 #define MAX_CUSTOM_SNIPPET_LONG_LENGTH 1024 #define INDENT_WIDTH 4 #define ArrayCount(A) sizeof(A)/sizeof(*(A)) #define Assert(Expression) do { if(!(Expression)){ Print(stderr, "l.%d: \e[1;31mAssertion failure\e[0m\n", __LINE__); __asm__("int3"); } } while(0) #define BreakHere() Print(stderr, "[%i] BreakHere\n", __LINE__) #define FOURCC(String) ((uint32_t)(String[0] << 0) | (uint32_t)(String[1] << 8) | (uint32_t)(String[2] << 16) | (uint32_t)(String[3] << 24)) #define Free(M) free(M); M = 0; #define FreeAndResetCount(M, Count) { Free(M); Count = 0; } #define MIN(A, B) A < B ? A : B #define MAX(A, B) A > B ? A : B #define Clamp(EndA, N, EndB) int Min = MIN(EndA, EndB); int Max = MAX(EndA, EndB); if(N < Min) { N = Min; } else if(N > Max) { N = Max; } typedef int32_t hash32; typedef hash32 asset_hash; typedef struct { union { int A; int Hours; }; union { int B; int Minutes; }; union { int C; int Seconds; }; union { int D; int Milliseconds; }; } v4; void Clear(void *V, uint64_t Size) { char *Ptr = (char *)V; for(int i = 0; i < Size; ++i) { *Ptr++ = 0; } } // NOTE(matt): Memory testing #include int GetUsage(void) { struct rusage Usage; getrusage(RUSAGE_SELF, &Usage); return Usage.ru_maxrss; } // NOTE(matt): Mem loop testing // // Let the usual work function happen, then: // // Call MEM_LOOP_PRE_FREE("Thing you're testing") // Call the freeing function // Call MEM_LOOP_PRE_WORK() // Call the function whose work the freeing function is meant to free // Call MEM_LOOP_POST("Thing you're testing") // #if DEBUG_MEMORY_LEAKAGE_LOOPED #define MEM_LOOP_PRE_FREE(String) \ Print(stderr, "Testing %s freeing\n", String);\ int MemLoopInitial = GetUsage();\ int MemLoopOld = MemLoopInitial;\ int MemLoopNew;\ for(int i = 0; i < 1 << 16; ++i)\ { #define MEM_LOOP_PRE_WORK() \ MemLoopNew = GetUsage();\ if(1 /*MemLoopNew > MemLoopOld*/) { Print(stderr, "iter %2i: %i (%s%i)\n", i, MemLoopNew, MemLoopNew > MemLoopOld ? "+" : "-", MemLoopNew - MemLoopOld); sleep(1); }\ #define MEM_LOOP_POST(String) \ MemLoopOld = MemLoopNew;\ }\ Print(stderr, "Done (%s): ", String);\ Colourise(MemLoopNew > MemLoopInitial ? CS_RED : CS_GREEN);\ Print(stderr, "%s%i\n", MemLoopNew > MemLoopInitial ? "+" : "-", MemLoopNew - MemLoopInitial);\ Colourise(CS_END);\ sleep(4); #else #define MEM_LOOP_PRE_FREE(String) #define MEM_LOOP_PRE_WORK() #define MEM_LOOP_POST(String) #endif // //// // NOTE(matt): One-time memory testing // #if DEBUG_MEMORY_LEAKAGE int DebugMemoryIndentLevel = 0; #define MEM_TEST_TOP() int MemTestInitialValue = GetUsage(); int MemTestRunningValue = MemTestInitialValue;\ /*\ Colourise(CS_BLACK_BOLD);\ Print(stderr, "[%5i]", __LINE__);\ Colourise(CS_GREEN);\ Print(stderr, " ");\ Indent(DebugMemoryIndentLevel);\ Print(stderr, "%s() starts at %i kb\n", __func__, MemTestInitialValue);\ Colourise(CS_END);\ */\ ++DebugMemoryIndentLevel; #define MEM_TEST_MID() if(GetUsage() > MemTestRunningValue)\ {\ Colourise(CS_BLACK_BOLD);\ Print(stderr, "[%5i]", __LINE__);\ Colourise(CS_YELLOW);\ Print(stderr, " ");\ Indent(DebugMemoryIndentLevel - 1);\ Print(stderr, "%s() is now at %i kb (+%i)\n", __func__, GetUsage(), GetUsage() - MemTestInitialValue);\ Colourise(CS_END);\ MemTestRunningValue = GetUsage();\ WaitForInput();\ } #define MEM_TEST_END() MemTestRunningValue = GetUsage();\ --DebugMemoryIndentLevel;\ if(MemTestRunningValue > MemTestInitialValue)\ {\ Colourise(CS_BLACK_BOLD);\ Print(stderr, "[%5i]", __LINE__);\ Colourise(CS_RED);\ Print(stderr, " ");\ Indent(DebugMemoryIndentLevel);\ Print(stderr, "%s() ends at %i kb (+%i)\n", __func__, MemTestRunningValue, MemTestRunningValue - MemTestInitialValue);\ Colourise(CS_END);\ WaitForInput();\ } #else #define MEM_TEST_TOP() #define MEM_TEST_MID() #define MEM_TEST_END() #endif // //// typedef enum { //MODE_FORCEINTEGRATION = 1 << 0, MODE_EXAMINE = 1 << 0, MODE_DRYRUN = 1 << 1, } mode; typedef enum { RC_ARENA_FULL, RC_ERROR_CAPACITY, RC_ERROR_DIRECTORY, RC_ERROR_FATAL, RC_ERROR_FILE, RC_ERROR_FILE_LOCKED, RC_ERROR_HMML, RC_ERROR_MAX_REFS, RC_ERROR_MEMORY, RC_ERROR_MISSING_PARAMETER, RC_ERROR_PARSING, RC_ERROR_PARSING_UNCLOSED_QUOTED_STRING, RC_ERROR_PROJECT, RC_ERROR_QUOTE, RC_ERROR_SEEK, RC_FOUND, RC_INIT, RC_INVALID_IDENTIFIER, RC_INVALID_REFERENCE, RC_INVALID_TEMPLATE, RC_NOOP, RC_PRIVATE_VIDEO, RC_RIP, RC_SCHEME_MIXTURE, RC_SYNTAX_ERROR, RC_UNFOUND, RC_UNHANDLED_REF_COMBO, RC_FAILURE, RC_SUCCESS } rc; typedef struct { void *Location; void *Ptr; char *ID; int Size; } arena; char *BufferIDStrings[] = { "BID_NULL", "BID_CHECKSUM", "BID_COLLATION_BUFFERS_CUSTOM0", "BID_COLLATION_BUFFERS_CUSTOM1", "BID_COLLATION_BUFFERS_CUSTOM2", "BID_COLLATION_BUFFERS_CUSTOM3", "BID_COLLATION_BUFFERS_CUSTOM4", "BID_COLLATION_BUFFERS_CUSTOM5", "BID_COLLATION_BUFFERS_CUSTOM6", "BID_COLLATION_BUFFERS_CUSTOM7", "BID_COLLATION_BUFFERS_CUSTOM8", "BID_COLLATION_BUFFERS_CUSTOM9", "BID_COLLATION_BUFFERS_CUSTOM10", "BID_COLLATION_BUFFERS_CUSTOM11", "BID_COLLATION_BUFFERS_CUSTOM12", "BID_COLLATION_BUFFERS_CUSTOM13", "BID_COLLATION_BUFFERS_CUSTOM14", "BID_COLLATION_BUFFERS_CUSTOM15", "BID_COLLATION_BUFFERS_INCLUDES_PLAYER", "BID_COLLATION_BUFFERS_INCLUDES_SEARCH", "BID_COLLATION_BUFFERS_PLAYER", "BID_COLLATION_BUFFERS_SEARCH", // malloc'd "BID_COLLATION_BUFFERS_SEARCH_ENTRY", "BID_COLLATION_BUFFERS_TITLE", "BID_COLLATION_BUFFERS_URL_PLAYER", "BID_COLLATION_BUFFERS_URL_SEARCH", "BID_COLLATION_BUFFERS_VIDEO_ID", "BID_COLLATION_BUFFERS_VOD_PLATFORM", "BID_DATABASE", "BID_ERRORS", "BID_FILTER", "BID_INDEX", "BID_INDEX_BUFFERS_CATEGORY_ICONS", "BID_INDEX_BUFFERS_CLASS", "BID_INDEX_BUFFERS_DATA", "BID_INDEX_BUFFERS_HEADER", "BID_INDEX_BUFFERS_MASTER", "BID_INDEX_BUFFERS_TEXT", "BID_INOTIFY_EVENTS", "BID_LINK", "BID_MASTER", "BID_MENU_BUFFERS_CREDITS", "BID_MENU_BUFFERS_FILTER", "BID_MENU_BUFFERS_FILTER_MEDIA", "BID_MENU_BUFFERS_FILTER_TOPICS", "BID_MENU_BUFFERS_QUOTE", "BID_MENU_BUFFERS_REFERENCE", "BID_NAVIGATION", "BID_NEXT_PLAYER_URL", "BID_PLAYER_BUFFERS_MAIN", "BID_PLAYER_BUFFERS_MENUS", "BID_PLAYER_BUFFERS_SCRIPT", "BID_PREVIOUS_PLAYER_URL", "BID_PWD_STRIPPED_PATH", "BID_QUOTE_STAGING", "BID_RESOLVED_PATH", "BID_SCRIPT", // malloc'd "BID_TOPICS", "BID_TO_PLAYER_URL", "BID_URL", "BID_URL_ASSET", "BID_URL_PLAYER", "BID_URL_SEARCH", "BID_VIDEO_API_RESPONSE", }; // TODO(matt): Note which buffers malloc typedef enum { BID_NULL, BID_CHECKSUM, BID_COLLATION_BUFFERS_CUSTOM0, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM1, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM2, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM3, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM4, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM5, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM6, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM7, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM8, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM9, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM10, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM11, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM12, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM13, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM14, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_CUSTOM15, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_INCLUDES_PLAYER, BID_COLLATION_BUFFERS_INCLUDES_SEARCH, BID_COLLATION_BUFFERS_PLAYER, BID_COLLATION_BUFFERS_SEARCH, // malloc'd BID_COLLATION_BUFFERS_SEARCH_ENTRY, BID_COLLATION_BUFFERS_TITLE, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_URL_PLAYER, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_URL_SEARCH, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_VIDEO_ID, // NOTE(matt): Not a buffer BID_COLLATION_BUFFERS_VOD_PLATFORM, // NOTE(matt): Not a buffer BID_DATABASE, // File.Buffer, malloc'd BID_ERRORS, BID_FILTER, // malloc'd BID_INDEX, // malloc'd BID_INDEX_BUFFERS_CATEGORY_ICONS, BID_INDEX_BUFFERS_CLASS, BID_INDEX_BUFFERS_DATA, BID_INDEX_BUFFERS_HEADER, BID_INDEX_BUFFERS_MASTER, BID_INDEX_BUFFERS_TEXT, BID_INOTIFY_EVENTS, BID_LINK, BID_MASTER, // NOTE(matt): This shall only ever be the destination, never the source. It is also malloc'd BID_MENU_BUFFERS_CREDITS, BID_MENU_BUFFERS_FILTER, BID_MENU_BUFFERS_FILTER_MEDIA, BID_MENU_BUFFERS_FILTER_TOPICS, BID_MENU_BUFFERS_QUOTE, BID_MENU_BUFFERS_REFERENCE, BID_NAVIGATION, BID_NEXT_PLAYER_URL, BID_PLAYER_BUFFERS_MAIN, BID_PLAYER_BUFFERS_MENUS, BID_PLAYER_BUFFERS_SCRIPT, BID_PREVIOUS_PLAYER_URL, BID_PWD_STRIPPED_PATH, BID_QUOTE_STAGING, // malloc'd BID_RESOLVED_PATH, BID_SCRIPT, // malloc'd BID_TOPICS, // File.Buffer, malloc'd BID_TO_PLAYER_URL, BID_URL, BID_URL_ASSET, BID_URL_PLAYER, BID_URL_SEARCH, BID_VIDEO_API_RESPONSE, BID_COUNT, } buffer_id; typedef struct { char *Location; char *Ptr; buffer_id ID; uint64_t Size; uint32_t IndentLevel; } buffer; typedef struct { buffer Buffer; char *Path; FILE *Handle; bool Locking; } file; char *ColourStrings[] = { "\e[0m", "\e[0;30m", "\e[0;31m", "\e[0;32m", "\e[0;33m", "\e[0;34m", "\e[0;35m", "\e[0;36m", "\e[0;37m", "\e[1;30m", "\e[1;31m", "\e[1;32m", "\e[1;33m", "\e[1;34m", "\e[1;35m", "\e[1;36m", "\e[1;37m", }; typedef enum { CS_END, CS_BLACK, CS_RED, CS_GREEN, CS_YELLOW, CS_BLUE, CS_MAGENTA, CS_CYAN, CS_WHITE, CS_BLACK_BOLD, CS_RED_BOLD, CS_GREEN_BOLD, CS_YELLOW_BOLD, CS_BLUE_BOLD, CS_MAGENTA_BOLD, CS_CYAN_BOLD, CS_WHITE_BOLD, } colour_code; #define CS_SUCCESS CS_GREEN_BOLD #define CS_ADDITION CS_GREEN_BOLD #define CS_REINSERTION CS_YELLOW_BOLD #define CS_FAILURE CS_RED_BOLD #define CS_ERROR CS_RED_BOLD #define CS_WARNING CS_YELLOW #define CS_PRIVATE CS_BLUE #define CS_ONGOING CS_MAGENTA #define CS_COMMENT CS_BLACK_BOLD #define CS_DELETION CS_BLACK_BOLD void Colourise(colour_code C) { Print(stderr, "%s", ColourStrings[C]); } uint64_t StringLength(char *String) { uint64_t i = 0; if(String) { while(String[i]) { ++i; } } return i; } // NOTE(matt): For use with 0-terminated strings string Wrap0(char *String) { string Result = {}; Result.Base = String; Result.Length = StringLength(String); return Result; } // NOTE(matt): For use with implicitly 0-terminated strings, i.e. fixed-size char arrays that may either be full of data, // or filled after their data with 0s #define Wrap0i(S) Wrap0i_(S, sizeof(S)) string Wrap0i_(char *S, uint64_t MaxSize) { string Result = {}; Result.Base = S; Result.Length = MaxSize; while(Result.Length > 0 && Result.Base[Result.Length - 1] == '\0') { --Result.Length; } return Result; } int64_t StringsDiffer(string A, string B) { int i = 0; while(i < A.Length && i < B.Length && A.Base[i] == B.Base[i]) { ++i; } if(i == A.Length && i == B.Length) { return 0; } else if(i == A.Length) { return -1; } else if(i == B.Length) { return 1; } return A.Base[i] - B.Base[i]; } int64_t StringsDifferLv0(string A, char *B) { if(B) { int i = 0; while(i < A.Length && B[i] && A.Base[i] == B[i]) { ++i; } if(i == A.Length && !B[i]) { return 0; } else if(i == A.Length) { return -1; } return A.Base[i] - B[i]; } else { return A.Length; } } int64_t StringsDifferS(char *NullTerminated, buffer *NotNullTerminated) { char *Ptr = NotNullTerminated->Ptr; uint64_t Length = StringLength(NullTerminated); int i = 0; while(i < Length && Ptr - NotNullTerminated->Location < NotNullTerminated->Size && *NullTerminated == *Ptr) { ++i, ++NullTerminated, ++Ptr; } if(i == Length) { return 0; } bool WithinBounds = Ptr - NotNullTerminated->Location < NotNullTerminated->Size; return WithinBounds ? *NullTerminated - *Ptr : *NullTerminated; } int64_t StringsDiffer0(char *A, char *B) { while(*A && *B && *A == *B) { ++A, ++B; } return *A - *B; } int StringsDifferCaseInsensitive(string A, string B) // NOTE(matt): Two null-terminated strings { int i = 0; while(i < A.Length && i < B.Length && ((A.Base[i] >= 'A' && A.Base[i] <= 'Z') ? A.Base[i] + ('a' - 'A') : A.Base[i]) == ((B.Base[i] >= 'A' && B.Base[i] <= 'Z') ? B.Base[i] + ('a' - 'A') : B.Base[i]) ) { ++i; } if(i == A.Length && i == B.Length) { return 0; } else if(i == A.Length) { return -1; } else if(i == B.Length) { return 1; } return ((A.Base[i] >= 'A' && A.Base[i] <= 'Z') ? A.Base[i] + ('a' - 'A') : A.Base[i]) - ((B.Base[i] >= 'A' && B.Base[i] <= 'Z') ? B.Base[i] + ('a' - 'A') : B.Base[i]); } int StringsDiffer0CaseInsensitive(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 >= 'A' && *A <= 'Z') ? *A + ('a' - 'A') : *A) - ((*B >= 'A' && *B <= 'Z') ? *B + ('a' - '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; } } bool StringsMatch(string A, string B) { int i = 0; while(i < A.Length && i < B.Length && A.Base[i] == B.Base[i]) { ++i; } if(i == A.Length && i == B.Length) { return TRUE; } return FALSE; } bool StringsMatchSized(string A, int Size, string B) { int i = 0; while(i < A.Length && i < B.Length && A.Base[i] == B.Base[i]) { ++i; } if(i == Size) { return TRUE; } return FALSE; } bool StringContains(string S, string Substring) { bool Result = FALSE; if(S.Length >= Substring.Length) { string Test = S; for(int64_t i = 0; i <= S.Length - Substring.Length; ++i, ++Test.Base, --Test.Length) { if(StringsMatchSized(Test, Substring.Length, Substring)) { Result = TRUE; break; } } } return Result; } int StringContainsXOfChar(string S, char C) { int Result = 0; for(int i = 0; i < S.Length; ++i) { Result += S.Base[i] == C; } return Result; } bool StringsMatchCaseInsensitive(string A, string B) { int i = 0; while(i < A.Length && i < B.Length && ((A.Base[i] >= 'A' && A.Base[i] <= 'Z') ? A.Base[i] + ('a' - 'A') : A.Base[i]) == ((B.Base[i] >= 'A' && B.Base[i] <= 'Z') ? B.Base[i] + ('a' - 'A') : B.Base[i])) { ++i; } if(i == A.Length && i == B.Length) { return TRUE; } return FALSE; } int CopyBytes(char *Dest, char *Src, int Count) { for(int i = 0; i < Count; ++i) { Dest[i] = Src[i]; } return Count; } int CopyStringToBarePtr(char *Dest, string Src) { return CopyBytes(Dest, Src.Base, Src.Length); } int CopyStringCToBarePtr(colour_code Colour, char *Dest, string Src) { int Length = 0; Length += CopyStringToBarePtr(Dest + Length, Wrap0(ColourStrings[Colour])); Length += CopyStringToBarePtr(Dest + Length, Src); Length += CopyStringToBarePtr(Dest + Length, Wrap0(ColourStrings[CS_END])); return Length; } memory_book InitBook(uint64_t DataWidthInBytes, uint64_t ItemsPerPage) { memory_book Result = {}; Result.PageSize = ItemsPerPage * DataWidthInBytes; Result.DataWidthInBytes = DataWidthInBytes; return Result; } memory_book InitBookOfPointers(uint64_t ItemsPerPage) { memory_book Result = {}; Result.PageSize = ItemsPerPage * sizeof(uintptr_t); Result.DataWidthInBytes = sizeof(uintptr_t); return Result; } memory_book InitBookOfStrings(uint64_t PageSizeInBytes) { memory_book Result = {}; Result.PageSize = PageSizeInBytes; Result.DataWidthInBytes = 1; return Result; } memory_page * AddPage(memory_book *M) { M->Pages = realloc(M->Pages, (M->PageCount + 1) * sizeof(memory_page)); M->Pages[M->PageCount].Base = calloc(1, M->PageSize); M->Pages[M->PageCount].Ptr = M->Pages[M->PageCount].Base; ++M->PageCount; return &M->Pages[M->PageCount - 1]; } void FreePage(memory_page *P) { Free(P->Base); P->Ptr = 0; } void FreePens(memory_book *M) { memory_pen_location *This = M->Pen; while(This) { M->Pen = M->Pen->Prev; Free(This); This = M->Pen; } } void FreeBook(memory_book *M) { for(int i = 0; i < M->PageCount; ++i) { FreePage(&M->Pages[i]); } FreeAndResetCount(M->Pages, M->PageCount); FreePens(M); memory_book Zero = {}; *M = Zero; } void FreeAndReinitialiseBook(memory_book *M) { uint64_t PageSize = M->PageSize; uint64_t DataWidthInBytes = M->DataWidthInBytes; FreeBook(M); M->PageSize = PageSize; M->DataWidthInBytes = DataWidthInBytes; } void ResetBook(memory_book *M) { for(int i = 0; i < M->PageCount; ++i) { memory_page *This = &M->Pages[i]; This->Ptr = This->Base; } FreePens(M); } memory_page * GetOrAddPageForString(memory_book *M, string S) { for(int i = 0; i < M->PageCount; ++i) { if((M->Pages[i].Ptr - M->Pages[i].Base) + S.Length < M->PageSize) { return &M->Pages[i]; } } return AddPage(M); } memory_page * GetOrAddPageForSize(memory_book *M, uint64_t Size) { for(int i = 0; i < M->PageCount; ++i) { if((M->Pages[i].Ptr - M->Pages[i].Base) + Size < M->PageSize) { return &M->Pages[i]; } } return AddPage(M); } string WriteStringInBook(memory_book *M, string S) { string Result = {}; memory_page *Page = GetOrAddPageForString(M, S); memory_pen_location *Pen = malloc(sizeof(memory_pen_location)); Pen->Page = Page; Pen->String.Base = Page->Ptr; Pen->String.Length = S.Length; Pen->Prev = M->Pen; Pen->Next = 0; if(M->Pen) { M->Pen->Next = Pen; } M->Pen = Pen; Result.Base = Page->Ptr; Result.Length = S.Length; Page->Ptr += CopyStringToBarePtr(Page->Ptr, S); return Result; } string ExtendStringInBook(memory_book *M, string S) { string Result = {}; if(M->Pen) { Result.Length = M->Pen->String.Length + S.Length; if(M->Pen->String.Base - M->Pen->Page->Base + Result.Length < M->PageSize) { M->Pen->Page->Ptr += CopyStringToBarePtr(M->Pen->Page->Ptr, S); } else { memory_pen_location Pen = *M->Pen; M->Pen->Page->Ptr = M->Pen->String.Base; memory_page *Page = GetOrAddPageForString(M, Result); M->Pen->Page = Page; M->Pen->String.Base = Page->Ptr; Page->Ptr += CopyStringToBarePtr(Page->Ptr, Pen.String); Page->Ptr += CopyStringToBarePtr(Page->Ptr, S); } Result.Base = M->Pen->String.Base; M->Pen->String.Length = Result.Length; } else { Result = WriteStringInBook(M, S); } return Result; } void ExtendStringCInBook(memory_book *M, colour_code Colour, string S) { ExtendStringInBook(M, Wrap0(ColourStrings[Colour])); ExtendStringInBook(M, S); ExtendStringInBook(M, Wrap0(ColourStrings[CS_END])); } typedef struct { int64_t Page; uint64_t Line; } book_position; book_position GetBookPositionFromIndex(memory_book *M, int Index) { book_position Result = {}; int ItemsPerPage = M->PageSize / M->DataWidthInBytes; Result.Line = Index % ItemsPerPage; Index -= Result.Line; Result.Page = Index / ItemsPerPage; return Result; } void * GetPlaceInBook(memory_book *M, int Index) { void *Result = 0; book_position Position = GetBookPositionFromIndex(M, Index); memory_page *Page = M->Pages + Position.Page; while((Position.Page + 1) > M->PageCount) { Page = AddPage(M); } char *Ptr = Page->Base; Ptr += M->DataWidthInBytes * Position.Line; Result = Ptr; return Result; } void * MakeSpaceInBook(memory_book *M) { return GetPlaceInBook(M, M->ItemCount++); } void ResetPen(memory_book *M) { if(M->Pen) { if(M->Pen->Page) { M->Pen->String.Base = M->Pen->Page->Ptr; } else { M->Pen->Page = AddPage(M); M->Pen->String.Base = M->Pen->Page->Base; } M->Pen->String.Length = 0; } } void EraseCurrentStringFromBook(memory_book *M) { M->Pen->Page->Ptr -= M->Pen->String.Length; if(M->Pen->Prev) { M->Pen->Prev->Next = M->Pen->Next; } if(M->Pen->Next) { M->Pen->Next->Prev = M->Pen->Prev; } memory_pen_location *This = M->Pen; M->Pen = M->Pen->Prev; Free(This); } bool PagesMatch(memory_page *A, memory_page *B) { return StringsMatch(Wrap0i_(A->Base, A->Ptr - A->Base), Wrap0i_(B->Base, B->Ptr - B->Base)); } bool BooksMatch(memory_book *A, memory_book *B) { bool Result = A->PageCount == B->PageCount; int i = 0; for(; Result == TRUE && i < A->PageCount; ++i) { if(!PagesMatch(&A->Pages[i], &B->Pages[i])) { Result = FALSE; break; } } if(Result == TRUE && i != A->PageCount) { Result = FALSE; } return Result; } void PrintPage(memory_page *P) { Print(stderr, "%.*s", (int)(P->Ptr - P->Base), P->Base); } void PrintBookOfStrings(memory_book *M) { for(int i = 0; i < M->PageCount; ++i) { PrintPage(&M->Pages[i]); } } int64_t StringToInt(string S) { int Result = 0; for(int i = 0; i < S.Length; ++i) { Result = Result * 10 + S.Base[i] - '0'; } return Result; } string ExtensionStrings[] = { {}, { ".gif", 4 }, { ".hmml", 5 }, { ".index", 6 }, { ".jpeg", 5 }, { ".jpg", 4 }, { ".png", 4 }, }; typedef enum { EXT_NULL, EXT_GIF, EXT_HMML, EXT_INDEX, EXT_JPEG, EXT_JPG, EXT_PNG, } extension_id; bool ExtensionMatches(string Path, extension_id Extension) // NOTE(matt): Extension includes the preceding "." { bool Result = FALSE; if(Path.Length >= ExtensionStrings[Extension].Length) { string Test = { .Base = Path.Base + Path.Length - ExtensionStrings[Extension].Length, .Length = ExtensionStrings[Extension].Length }; if(StringsMatchCaseInsensitive(Test, ExtensionStrings[Extension])) { Result = TRUE; } } return Result; } void ExtendString0(char **Dest, string Src) { if(Src.Length > 0) { uint64_t OriginalLength = 0; if(*Dest) { OriginalLength = StringLength(*Dest); } uint64_t RequiredBytes = OriginalLength + Src.Length + 1; *Dest = realloc(*Dest, RequiredBytes); char *Ptr = *Dest + OriginalLength; for(int i = 0; i < Src.Length; ++i) { *Ptr++ = Src.Base[i]; } *Ptr = '\0'; } } file InitFile(string *Directory, string *Filename, extension_id Extension, bool Locking) { file Result = {}; if(Directory) { ExtendString0(&Result.Path, *Directory); ExtendString0(&Result.Path, Wrap0("/")); } ExtendString0(&Result.Path, *Filename); if(Extension != EXT_NULL) { ExtendString0(&Result.Path, ExtensionStrings[Extension]); } Result.Locking = Locking; return Result; } void CloseFile(file *F, bool CloseLocking) { if(F->Handle) { fclose(F->Handle); F->Handle = 0; if(F->Locking && !CloseLocking) { F->Handle = fopen(F->Path, "r"); flock(fileno(F->Handle), LOCK_EX | LOCK_NB); } } } bool TryLock(file *F) // USAGE: File shall already be open, if possible { bool Result = TRUE; if(F->Locking && F->Handle) { if(flock(fileno(F->Handle), LOCK_EX | LOCK_NB) == -1) { Result = FALSE; } } return Result; } rc ReadFileIntoBuffer(file *F) { rc Result = RC_ERROR_FILE; if(F->Path) { if(F->Handle || (F->Handle = fopen(F->Path, "r"))) { if(TryLock(F)) { fseek(F->Handle, 0, SEEK_END); F->Buffer.Size = ftell(F->Handle); F->Buffer.Location = malloc(F->Buffer.Size); F->Buffer.Ptr = F->Buffer.Location; fseek(F->Handle, 0, SEEK_SET); fread(F->Buffer.Location, F->Buffer.Size, 1, F->Handle); CloseFile(F, FALSE); Result = RC_SUCCESS; } else { Result = RC_ERROR_FILE_LOCKED; } } } return Result; } char * ReadFileIntoMemory0(FILE *F) { char *Result = 0; if(F) { fseek(F, 0, SEEK_END); int Length = ftell(F); Result = malloc(Length + 1); fseek(F, 0, SEEK_SET); fread(Result, Length, 1, F); Result[Length] = 0; } return Result; } int PrintString(string S) { return Print(stderr, "%.*s", (int)S.Length, S.Base); } void PrintStringN(string S) { Print(stderr, "\n%.*s", (int)S.Length, S.Base); } void PrintStringI(string S, uint64_t Indentation) { for(int i = 0; i < Indentation; ++i) { Print(stderr, " "); } PrintString(S); } int PrintStringC(colour_code Colour, string String) { Colourise(Colour); int Result = PrintString(String); Colourise(CS_END); return Result; } void PrintStringCN(colour_code Colour, string String, bool PrependNewline, bool AppendNewline) { if(PrependNewline) { Print(stderr, "\n"); } Colourise(Colour); PrintString(String); Colourise(CS_END); if(AppendNewline) { Print(stderr, "\n"); } } void PrintC(colour_code Colour, char *String) { Colourise(Colour); Print(stderr, "%s", String); Colourise(CS_END); } #if DEBUG_PRINT_FUNCTION_NAMES void PrintFunctionName(char *N) { PrintC(CS_MAGENTA, N); Print(stderr, "\n"); } void PrintLinedFunctionName(int LineNumber, char *N) { Colourise(CS_BLACK_BOLD); Print(stderr, "[%i] ", LineNumber); Colourise(CS_END); PrintFunctionName(N); } #else #define PrintFunctionName(S); #define PrintLinedFunctionName(L, N); #endif typedef enum { TOKEN_NULL, TOKEN_COMMENT_SINGLE, TOKEN_COMMENT_MULTI_OPEN, TOKEN_COMMENT_MULTI_CLOSE, TOKEN_MINUS, TOKEN_ASSIGN, TOKEN_SEMICOLON, TOKEN_OPEN_BRACE, TOKEN_CLOSE_BRACE, TOKEN_DOUBLEQUOTE, TOKEN_NEWLINE, TOKEN_COMMENT, TOKEN_STRING, // Double-quoted alphanumeric, may contain spaces TOKEN_IDENTIFIER, // Alphanumeric TOKEN_NUMBER, // Numeric //TOKEN_UNHANDLED, // TODO(matt): Consider the need for this } token_type; char *TokenTypeStrings[] = { "", "COMMENT_SINGLE", "COMMENT_MULTI_OPEN", "COMMENT_MULTI_CLOSE", "MINUS", "ASSIGN", "SEMICOLON", "OPEN_BRACE", "CLOSE_BRACE", "DOUBLEQUOTE", "NEWLINE", "COMMENT", "STRING", // Double-quoted alphanumeric, may contain spaces "IDENTIFIER", // Alphanumeric "NUMBER", // Numeric }; char *TokenStrings[] = { "", "//", "/*", "*/", "-", "=", ";", "{", "}", "\"", "\n", "Comment", "String", // Double-quoted alphanumeric, may contain spaces "Identifier", // Alphanumeric "Number", // Numeric }; typedef enum { FID_NULL, FID_METADATA, } file_id; typedef struct { uint64_t BytePosition; int64_t Size; } file_edit; typedef enum { T_db_header_project, T_db_entry, T_db_block_assets, } type_id; typedef struct { type_id ID; void *Ptr; uint64_t Byte; } file_signpost; typedef struct { file_id ID; file_edit Edit; file_signpost ProjectsBlock; file_signpost ProjectParent; file_signpost ProjectHeader; file_signpost Prev; file_signpost This; file_signpost Next; file_signpost AssetsBlock; } file_signposts; typedef struct { file File; file_signposts Signposts; } file_signposted; void WriteFromByteToPointer(file *F, uint64_t *BytesThroughBuffer, void *Pointer) { uint64_t Byte = (char *)Pointer - F->Buffer.Location; fwrite(F->Buffer.Location + *BytesThroughBuffer, Byte - *BytesThroughBuffer, 1, F->Handle); *BytesThroughBuffer += Byte - *BytesThroughBuffer; } uint64_t WriteFromPointerToPointer(file *F, void *From, void *To, uint64_t *BytesWritten) { uint64_t BytesToWrite = (char *)To - (char *)From; fwrite(From, BytesToWrite, 1, F->Handle); *BytesWritten += BytesToWrite; return BytesToWrite; } uint64_t WriteFromByteToEnd(file *F, uint64_t Byte) { fwrite(F->Buffer.Location + Byte, F->Buffer.Size - Byte, 1, F->Handle); return F->Buffer.Size - Byte; } typedef struct { char *String; char *IdentifierDescription; char *IdentifierDescription_Medium; char *LocalVariableDescription; char *IdentifierDescription_Role; char *IdentifierDescription_Credit; bool IdentifierDescriptionDisplayed; bool IdentifierDescription_MediumDisplayed; bool LocalVariableDescriptionDisplayed; bool IdentifierDescription_RoleDisplayed; bool IdentifierDescription_CreditDisplayed; } config_identifier; config_identifier ConfigIdentifiers[] = { { "" }, { "abbrev_dotted_initial_and_surname", "The dotted-initial(s) and surname of a person's name, used to override the auto-derived one, e.g. J. R. R. Tolkien (from John Ronald Reuel Tolkien) or J. du Pré (from Jacqueline du Pré)" }, { "abbrev_given_or_nickname", "The given or quoted nickname of a person's name, used to override the auto-derived one, e.g. Charlotte (from Charlotte Brontë) or Ry (from Ryland Peter \"Ry\" Cooder)" }, { "abbrev_initial", "The initials of a person's name, used to override the auto-derived ones, e.g. KB (from Kate Bush)" }, { "allow", "An include-rule string of the forms: \"identifier\", \"type.member\" or \"type.member.member\", etc. For example:\n\nallow = \"project.default_medium\";\n\nAdding an \"allow\" / \"deny\" rule makes the inclusion prohibitive or permissive, respectively, \ and they cannot be mixed (i.e. only \"allow\" rules, or only \"deny\" rules)." }, { "art" }, { "art_variants" }, { "assets_root_dir", "Absolute directory path, from where the locations of CSS, JavaScript and image files are derived (see also css_path, images_path and js_path). Setting this correctly allows Cinera to read in the \ files for the purpose of hashing them." }, { "assets_root_url", "URL from where the locations of CSS, JavaScript and image files are derived (see also css_path, images_path and js_path). Setting this correctly allows HTTP requests for these files to be fulfilled." }, { "base_dir", "Absolute directory path, where Cinera will generate the search page (see also search_location) and directories for the player pages (see also player_location)." }, { "base_url", "URL where the search page (see also search_location) and player pages (see also player_location) will be located." }, { "cache_dir", "Internal directory (i.e. no access from the wider internet) where we store errors.log and quotes retrieved from insobot." }, { "cohost", "The ID of a person (see also person) who cohosts a project. They will then appear in the credits menu of each entry in the project. Note that setting a cohost in the configuration \ file credits this person for the entire project, but indexers may \"uncredit\" people in the [video] node of a HMML file." }, { "credit", "The ID of a person (see also person) who contributed to a project. They will then appear in the credits menu of each entry in the project. Note that setting a credit in the configuration \ file credits this person for the entire project, but indexers may \"uncredit\" people in the [video] node of a HMML file." }, { "css_path", "Path relative to assets_root_dir and assets_root_url where CSS files are located." }, { "db_location", "Absolute file path where the database file resides. If you run multiple instances of Cinera on the same machine, please ensure this db_location differs between them." }, { "default_cc_lang", "The ISO 639-1 language code of the closed captions to enable by default. May be overridden by setting the cc_lang in the video node of an HMML file." }, { "default_medium", "The ID of a medium (see also medium) which will be the default for the project. May be overridden by setting the medium in the video node of an HMML file." }, { "deny", "See allow." }, { "deny_bespoke_templates", "Indexers may use the \"template\" attribute in the video node of an .hmml file to set a bespoke template for that entry, superseding any configured player_template. Setting \"deny_bespoke_templates\" to true prevents this." }, { "genre", "This is a setting for the future. We currently only support the \"video\" genre, which is the default." }, { "global_search_dir", "Absolute directory path, where the global search page will be located, collating the search pages of all projects in the Cinera instance." }, { "global_search_template", "Path of a HTML template file relative to the global_templates_dir from which the global search page will be generated." }, { "global_search_url", "URL where the global search page will be located, collating the search pages of all projects in the Cinera instance." }, { "global_templates_dir", "Absolute directory path, from where the global_search_template path may be derived." }, { "global_theme", "The theme used for global pages, e.g. search page. As with all themes, its name forms the file path of a CSS file containing the style as follows: cinera__${theme}.css" }, { "guest", "The ID of a person (see also person) who guests in a project. They will then appear in the credits menu of each entry in the project. Note that setting a guest in the configuration \ file credits this person for the entire project, but indexers may \"uncredit\" people in the [video] node of a HMML file." }, { "hidden", "Hidden media are filtered off by default in the player. For example, we may create an \"afk\" medium to tag portions of videos where the host is \"Away from \ Keyboard\". These portions, filtered off, will be skipped to save the viewer sitting through them." }, { "hmml_dir", "The input directory where Cinera will look for the project's .hmml files." }, { "homepage", "The URL where visitors will find the person's homepage." }, { "html_title", "The HTML \"enhanced\" version of title, which may employ HTML tags." }, { "icon", "The file path of an image that will appear in the credits menu beside the name of a person who may be supported via this support platform. The image shall be a 32×16 pixel sprite, with the left side \ used when the image is not focused, and the right side when it is focused.", "A HTML character entity used to denote the medium in the filter menu and list of indices." }, { "icon_disabled" }, { "icon_focused" }, { "icon_normal" }, { "icon_type" }, { "icon_variants" }, { "ignore_privacy", "Creators may be able to set the publication status of their content. By default Cinera respects this status by not processing any content that is private. If a creator only publishes \ publicly, then we can set ignore_privacy to \"true\" and save the resources otherwise spent checking the privacy status."}, { "images_path", "Path relative to assets_root_dir and assets_root_url where image files are located.", 0 }, { "include", "The path - either absolute or relative to the containing config file - of a file to be included in the configuration." }, { "indexer", "The ID of a person (see also person) who indexes a project. They will then appear in the credits menu of each entry in the project. Note that setting an indexer in the configuration \ file credits this person for the entire project, but indexers may \"uncredit\" people in the [video] node of a HMML file." }, { "instance_title", "The name of the instance, used as the cinera:title on the global search page." }, { "js_path", "Path relative to assets_root_dir and assets_root_url where JavaScript files are located." }, { "lineage", 0, 0, "A slash-separated string of all project IDs from the top of the family tree to the present project." }, { "lineage_without_origin", 0, 0, "Same as the $lineage, without the first component (the $origin)." }, { "log_level", "Possible log levels, from least to most verbose: \"emergency\", \"alert\", \"critical\", \"error\", \"warning\", \"notice\", \"informational\", \"debug\"."}, { "medium", "In HMML an indexer may prefix a word with a colon, to mark it as a category. This category will then appear in the filter menu, for viewers to toggle on / off. A category may be either a topic (by \ default categories are assumed to be topics) or a medium, and both use the same colon prefix. Configuring a medium is our way of stating that a category is indeed a medium, not a topic." }, { "name", "The person's name as it appears in the credits menu.", "The name of the medium as it appears in the filter menu and in a tooltip when hovering on the medium's icon in an index item.", 0, "The name of the role as it appears in the credits menu." }, { "non-speaking", "We try to abbreviate the names of speakers to cite them more concisely in the timestamps. Set this to \"true\" to prevent abbreviating the names of people in non-speaking roles, and so avoid erroneous clashes." }, { "numbering_filename_prefix", "This works in conjunction with the \"filename_derived\" numbering_method. We take the base filename, strip this prefix (default \"$project\") from the start and use the rest as the number." }, { "numbering_method", "Possible numbering methods: \"auto\" (see also numbering_start and numbering_zero_pad), \"filename_derived\" (see also numbering_filename_prefix), \"hmml_specified\"." }, { "numbering_scheme", "Possible numbering schemes: \"calendrical\", \"linear\" (the default), \"seasonal\". Currently this setting does nothing." }, { "numbering_start", "This works in conjunction with the \"auto\" numbering scheme. The number (default 1) denotes the displayed position of the project's first entry recorded in Cinera. Setting it to 0 makes the entries 0-indexed." }, { "numbering_unit", "This is a freely-configurable string - e.g. \"Day\", \"Session\", \"Episode\", \"Meeting\" - which is written on the search page, \ preceding the derived number of each entry. If the unit is not set, then the entries will not be numbered." }, { "numbering_zero_pad", "This works in conjunction with the \"auto\" numbering scheme. It zero-pads the entries' numbers so that they all, from the smallest to largest, contain the same number of digits." }, { "origin", 0, 0, "The ID of the project in our branch of the family tree which has no parent." }, { "owner", "The ID of the person (see also person) who owns the project. There may only be one owner, and they will appear in the credits menu as the host of each entry.", 0, "The ID of the project's owner." }, { "person", "This is someone who played a role in the projects, for which they deserve credit (see also: credit).", 0, "The ID of the person within which scope the variable occurs." }, { "player_location", "The location of the project's player pages relative to base_dir and base_url." }, { "player_template", "Path of a HTML template file relative to the templates_dir from which the project's player pages will be generated." }, { "plural", "The irregular plural form of the role's name. May be left blank for regular plurals (in English, those ending in \"s\")." }, { "position", "The position in the credits list in which this role should reside. Positive numbers position the role from the list's start. Negative numbers from the end. Roles lacking a defined position \ fill the slots left vacant by positioned roles in the order in which they are configured.", }, { "privacy_check_interval", "In minutes, this sets how often to check if the privacy status of private entries has changed to \"public\"." }, { "project", "The work horse of the whole configuration. A config file lacking project scopes will produce no output. Notably project scopes may themselves contain project scopes.", 0, "The ID of the project within which scope the variable occurs." }, { "quote_username", "The username by which insobot recognises a person, for the purpose of retrieving quotes attributed to them." }, { "role", "This is the role for which a person deserves credit (see also: credit).", 0, 0, 0, "The ID of the role within which scope the variable occurs." }, { "query_string", "This string (default \"r\") enables web browsers to cache asset files. We hash those files to produce a number, which we then write to HTML files in hexadecimal format, e.g. \ ?r=a59bb130. Hashing may be disabled by setting query_string = \"\";" }, { "search_location", "The location of the project's search page relative to base_dir and base_url." }, { "search_template", "Path of a HTML template file relative to the templates_dir from which the project's search page will be generated." }, { "single_browser_tab", "Setting this to \"true\" (default \"false\") makes the search page open player pages in its own tab." }, { "stream_platform", "This is a setting for the future. We currently only support \"twitch\" but not in any meaningful way." }, { "support", "Information detailing where a person may be supported, to be cited in the credits menu.", 0, "The ID of the support platform within which scope the variable occurs." }, { "suppress_prompts", "When issuing an error, Cinera prompts the user, giving them time to read the error. Setting this to \"true\" prevents this behaviour." }, { "templates_dir", "Absolute directory path, from where the player_template and search_template path may be derived." }, { "theme", "The theme used to style all the project's pages. As with all themes, its name forms the file path of a CSS file containing the style as follows: cinera__${theme}.css" }, { "title", "The full name of the project." }, { "title_list_delimiter", "Currently not implemented, probably to be removed." }, { "title_list_end", "Currently not implemented, probably to be removed." }, { "title_list_prefix", "Currently not implemented, probably to be removed." }, { "title_list_suffix", "Currently not implemented, probably to be removed."} , { "title_suffix", "Currently not implemented, probably to be removed." }, { "vod_platform", "Possible VOD platforms: \"direct\" (for .webm, .mp4, etc. files), \"vimeo\", \"youtube\"." }, { "url", "The URL where viewers may support the person, e.g. their page on a crowd funding site, the \"pledge\" page on their own website." }, }; typedef enum { IDENT_NULL, IDENT_ABBREV_DOTTED_INITIAL_AND_SURNAME, IDENT_ABBREV_GIVEN_OR_NICKNAME, IDENT_ABBREV_INITIAL, IDENT_ALLOW, IDENT_ART, IDENT_ART_VARIANTS, IDENT_ASSETS_ROOT_DIR, IDENT_ASSETS_ROOT_URL, IDENT_BASE_DIR, IDENT_BASE_URL, IDENT_CACHE_DIR, IDENT_COHOST, IDENT_CREDIT, IDENT_CSS_PATH, IDENT_DB_LOCATION, IDENT_DEFAULT_CC_LANG, IDENT_DEFAULT_MEDIUM, IDENT_DENY, IDENT_DENY_BESPOKE_TEMPLATES, IDENT_GENRE, IDENT_GLOBAL_SEARCH_DIR, IDENT_GLOBAL_SEARCH_TEMPLATE, IDENT_GLOBAL_SEARCH_URL, IDENT_GLOBAL_TEMPLATES_DIR, IDENT_GLOBAL_THEME, IDENT_GUEST, IDENT_HIDDEN, IDENT_HMML_DIR, IDENT_HOMEPAGE, IDENT_HTML_TITLE, IDENT_ICON, IDENT_ICON_DISABLED, IDENT_ICON_FOCUSED, IDENT_ICON_NORMAL, IDENT_ICON_TYPE, IDENT_ICON_VARIANTS, IDENT_IGNORE_PRIVACY, IDENT_IMAGES_PATH, IDENT_INCLUDE, IDENT_INDEXER, IDENT_INSTANCE_TITLE, IDENT_JS_PATH, IDENT_LINEAGE, IDENT_LINEAGE_WITHOUT_ORIGIN, IDENT_LOG_LEVEL, IDENT_MEDIUM, IDENT_NAME, IDENT_NON_SPEAKING, IDENT_NUMBERING_FILENAME_PREFIX, IDENT_NUMBERING_METHOD, IDENT_NUMBERING_SCHEME, IDENT_NUMBERING_START, IDENT_NUMBERING_UNIT, IDENT_NUMBERING_ZERO_PAD, IDENT_ORIGIN, IDENT_OWNER, IDENT_PERSON, IDENT_PLAYER_LOCATION, IDENT_PLAYER_TEMPLATE, IDENT_PLURAL, IDENT_POSITION, IDENT_PRIVACY_CHECK_INTERVAL, IDENT_PROJECT, IDENT_QUOTE_USERNAME, IDENT_ROLE, IDENT_QUERY_STRING, IDENT_SEARCH_LOCATION, IDENT_SEARCH_TEMPLATE, IDENT_SINGLE_BROWSER_TAB, IDENT_STREAM_PLATFORM, IDENT_SUPPORT, IDENT_SUPPRESS_PROMPTS, IDENT_TEMPLATES_DIR, IDENT_THEME, IDENT_TITLE, IDENT_TITLE_LIST_DELIMITER, IDENT_TITLE_LIST_END, IDENT_TITLE_LIST_PREFIX, IDENT_TITLE_LIST_SUFFIX, IDENT_TITLE_SUFFIX, IDENT_VOD_PLATFORM, IDENT_URL, IDENT_COUNT, } config_identifier_id; void Indent(uint64_t Indent) { for(int i = 0; i < INDENT_WIDTH * Indent; ++i) { Print(stderr, " "); } } void IndentedCarriageReturn(int IndentationLevel) { Print(stderr, "\n"); Indent(IndentationLevel); } void AlignText(int Alignment) { for(int i = 0; i < Alignment; ++i) { Print(stderr, " "); } } void NewParagraph(int IndentationLevel) { Print(stderr, "\n"); IndentedCarriageReturn(IndentationLevel); } void NewSection(char *Title, int *IndentationLevel) { IndentedCarriageReturn(*IndentationLevel); if(Title) { Print(stderr, "%s:", Title); } ++*IndentationLevel; IndentedCarriageReturn(*IndentationLevel); } void EndSection(int *IndentationLevel) { --*IndentationLevel; } int GetTerminalColumns(void) { struct winsize TermDim; ioctl(STDOUT_FILENO, TIOCGWINSZ, &TermDim); return TermDim.ws_col; } void ClearTerminal(void) { Print(stderr, "\033[2J"); } void TypesetString(int CurrentColumn, string String) { if(String.Length > 0) { int TermCols = GetTerminalColumns(); int AvailableCharacters = TermCols - CurrentColumn; int CharactersToWrite = String.Length; string Pen = { .Base = String.Base + String.Length - CharactersToWrite, .Length = MIN(String.Length, AvailableCharacters) }; int Length = Pen.Length; while(Length > 0 && Length < String.Length - (String.Length - CharactersToWrite) && Pen.Base[Length] != ' ') { --Length; } bool SplitAtWhitespace = FALSE; if(Length > 0) { if(Length < String.Length - (String.Length - CharactersToWrite) && Pen.Base[Length] == ' ') { SplitAtWhitespace = TRUE; } Pen.Length = Length; } for(int i = 0; i < Pen.Length; ++i) { if(Pen.Base[i] == '\n') { Pen.Length = i; SplitAtWhitespace = TRUE; break; } } PrintString(Pen); CharactersToWrite -= Pen.Length; if(SplitAtWhitespace) { --CharactersToWrite; } while(CharactersToWrite > 0) { Print(stderr, "\n"); for(int i = 0; i < CurrentColumn; ++i) { Print(stderr, " "); } string Pen = { .Base = String.Base + String.Length - CharactersToWrite, .Length = MIN(CharactersToWrite, AvailableCharacters) }; Length = Pen.Length; while(Length > 0 && Length < String.Length - (String.Length - CharactersToWrite) && Pen.Base[Length] != ' ') { --Length; } SplitAtWhitespace = FALSE; if(Length > 0) { if(Length < String.Length - (String.Length - CharactersToWrite) && Pen.Base[Length] == ' ') { SplitAtWhitespace = TRUE; } Pen.Length = Length; } for(int i = 0; i < Pen.Length; ++i) { if(Pen.Base[i] == '\n') { Pen.Length = i; SplitAtWhitespace = TRUE; break; } } PrintString(Pen); CharactersToWrite -= Pen.Length; if(SplitAtWhitespace) { --CharactersToWrite; } } } } typedef struct { token_type Type; string Content; int64_t int64_t; uint64_t LineNumber; } token; typedef struct { file File; _memory_book(token) Token; uint64_t CurrentIndex; uint64_t CurrentLine; } tokens; char *ArtVariantStrings[] = { "light_normal", "light_focused", "light_disabled", "dark_normal", "dark_focused", "dark_disabled", }; typedef enum { AVS_LIGHT_NORMAL, AVS_LIGHT_FOCUSED, AVS_LIGHT_DISABLED, AVS_DARK_NORMAL, AVS_DARK_FOCUSED, AVS_DARK_DISABLED, AVS_COUNT, } art_variant_shifters; typedef struct { char *String; uint8_t Mapping:6; uint8_t Unused:2; } config_art_variant; config_art_variant ConfigArtVariants[] = { { }, { "light_normal", 1 << AVS_LIGHT_NORMAL }, { "light_focused", 1 << AVS_LIGHT_FOCUSED }, { "light_disabled", 1 << AVS_LIGHT_DISABLED }, { "light", ((1 << AVS_LIGHT_NORMAL) | (1 << AVS_LIGHT_FOCUSED) | (1 << AVS_LIGHT_DISABLED)) }, { "dark_normal", 1 << AVS_DARK_NORMAL }, { "dark_focused", 1 << AVS_DARK_FOCUSED }, { "dark_disabled", 1 << AVS_DARK_DISABLED }, { "dark", ((1 << AVS_DARK_NORMAL) | (1 << AVS_DARK_FOCUSED) | (1 << AVS_DARK_DISABLED)) }, { "normal", ((1 << AVS_LIGHT_NORMAL) | (1 << AVS_DARK_NORMAL)) }, { "focused", ((1 << AVS_LIGHT_FOCUSED) | (1 << AVS_DARK_FOCUSED)) }, { "disabled", ((1 << AVS_LIGHT_NORMAL) | (1 << AVS_DARK_NORMAL)) }, { "all", ((1 << AVS_LIGHT_NORMAL) | (1 << AVS_LIGHT_FOCUSED) | (1 << AVS_LIGHT_DISABLED) | (1 << AVS_DARK_NORMAL) | (1 << AVS_DARK_FOCUSED) | (1 << AVS_DARK_DISABLED)) }, }; typedef enum { CAV_DEFAULT_UNSET, CAV_LIGHT_NORMAL, CAV_LIGHT_FOCUSED, CAV_LIGHT_DISABLED, CAV_LIGHT, CAV_DARK_NORMAL, CAV_DARK_FOCUSED, CAV_DARK_DISABLED, CAV_DARK, CAV_NORMAL, CAV_FOCUSED, CAV_DISABLED, CAV_ALL, CAV_COUNT, } config_art_variant_id; char *SeverityStrings[] = { "error", "warning", }; typedef enum { S_ERROR, S_WARNING, } severity; char *ErrorDomainStrings[] = { "Config", "Indexing", "System", }; typedef enum { ED_CONFIG, ED_INDEXING, ED_SYSTEM, } error_domain; void ErrorFilenameAndLineNumber(string *Filename, uint64_t LineNumber, severity Severity, error_domain Domain) { Print(stderr, "\n" "┌─ "); switch(Severity) { case S_ERROR: Colourise(CS_ERROR); break; case S_WARNING: Colourise(CS_WARNING); break; } Print(stderr, "%s %s", ErrorDomainStrings[Domain], SeverityStrings[Severity]); Colourise(CS_END); if(Filename) { if(LineNumber > 0) { Print(stderr, " on line "); Colourise(CS_BLUE_BOLD); Print(stderr, "%lu", LineNumber); Colourise(CS_END); Print(stderr, " of "); } else { Print(stderr, " with "); } PrintStringC(CS_CYAN, *Filename); } Print(stderr, "\n" "└─╼ "); } // TODO(matt): Get more errors going through Error() void ConfigError(string *Filename, uint64_t LineNumber, severity Severity, char *Message, string *Received) { ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_CONFIG); Print(stderr, "%s", Message); if(Received) { PrintStringC(CS_MAGENTA_BOLD, *Received); } Print(stderr, "\n"); } void ConfigErrorField(string *Filename, uint64_t LineNumber, severity Severity, config_identifier_id FieldID, string *Received, char *Message) { ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_CONFIG); Print(stderr, "%s%s%s value %s%.*s%s %s\n", ColourStrings[CS_YELLOW_BOLD], ConfigIdentifiers[FieldID].String, ColourStrings[CS_END], ColourStrings[CS_MAGENTA_BOLD], (int)Received->Length, Received->Base, ColourStrings[CS_END], Message); } void ConfigErrorUnset(config_identifier_id FieldID) { ErrorFilenameAndLineNumber(0, 0, S_ERROR, ED_CONFIG); Print(stderr, "Unset %s\n", ConfigIdentifiers[FieldID].String); } void ConfigErrorUnsetFieldOf(string *Filename, uint64_t LineNumber, config_identifier_id UnsetFieldID, config_identifier_id ScopeKey, string ScopeID) { ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_CONFIG); Print(stderr, "Unset %s%s%s of %s%s%s: %s%.*s%s\n", ColourStrings[CS_YELLOW_BOLD], ConfigIdentifiers[UnsetFieldID].String, ColourStrings[CS_END], ColourStrings[CS_YELLOW_BOLD], ConfigIdentifiers[ScopeKey].String, ColourStrings[CS_END], ColourStrings[CS_GREEN_BOLD], (int)ScopeID.Length, ScopeID.Base, ColourStrings[CS_END]); } void ConfigErrorSizing(string *Filename, uint64_t LineNumber, config_identifier_id FieldID, string *Received, uint64_t MaxSize) { ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_CONFIG); Print(stderr, "%s value is too long (%lu/%lu characters): ", ConfigIdentifiers[FieldID].String, Received->Length, MaxSize); PrintStringC(CS_MAGENTA_BOLD, *Received); Print(stderr, "\n"); } void ConfigErrorLockedConfigLocation(string *Filename, uint64_t LineNumber, string *Received) { ErrorFilenameAndLineNumber(Filename, LineNumber, Filename ? S_WARNING : S_ERROR, ED_CONFIG); Print(stderr, "%s file %s%.*s%s is in use by another Cinera instance\n", Filename ? "Included" : "Config", ColourStrings[CS_MAGENTA_BOLD], (int)Received->Length, Received->Base, ColourStrings[CS_END]); } void ConfigErrorUnopenableConfigLocation(string *Filename, uint64_t LineNumber, string *Received) { ErrorFilenameAndLineNumber(Filename, LineNumber, Filename ? S_WARNING : S_ERROR, ED_CONFIG); Print(stderr, "%s file %s%.*s%s could not be opened: %s\n", Filename ? "Included" : "Config", ColourStrings[CS_MAGENTA_BOLD], (int)Received->Length, Received->Base, ColourStrings[CS_END], strerror(errno)); } void ConfigErrorLockedDBLocation(string *Filename, uint64_t LineNumber, string *Received) { ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_CONFIG); Print(stderr, "File at db_location %s%.*s%s is in use by another Cinera instance\n", ColourStrings[CS_MAGENTA_BOLD], (int)Received->Length, Received->Base, ColourStrings[CS_END]); } void ConfigErrorInt(string *Filename, uint64_t LineNumber, severity Severity, char *Message, uint64_t Number) { ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_CONFIG); Print(stderr, "%s%s%lu%s\n", Message, ColourStrings[CS_BLUE_BOLD], Number, ColourStrings[CS_END]); } void ConfigErrorExpectation(tokens *T, token_type GreaterExpectation, token_type LesserExpectation) { token *This = GetPlaceInBook(&T->Token, T->CurrentIndex); string Filepath = Wrap0(T->File.Path); ErrorFilenameAndLineNumber(&Filepath, This->LineNumber, S_ERROR, ED_CONFIG); Print(stderr, "Syntax error: Received "); if(This->Content.Base) { PrintStringC(CS_RED, This->Content); } else { Print(stderr, "%s%ld%s", ColourStrings[CS_BLUE_BOLD], This->int64_t, ColourStrings[CS_END]); } Print(stderr, " but Expected "); PrintStringC(CS_GREEN, Wrap0(TokenStrings[GreaterExpectation])); if(LesserExpectation) { Print(stderr, " or "); PrintStringC(CS_GREEN, Wrap0(TokenStrings[LesserExpectation])); } Print(stderr, "\n"); } void IndexingError(string Filename, uint64_t LineNumber, severity Severity, char *Message, string *Received) { ErrorFilenameAndLineNumber(&Filename, LineNumber, Severity, ED_INDEXING); // TODO(matt): Typeset the Message? Print(stderr, "%s", Message); if(Received) { PrintStringC(CS_MAGENTA_BOLD, *Received); } Print(stderr, "\n"); } void PrintTimecode(FILE *Dest, v4 Timecode) { Colourise(CS_BLUE_BOLD); if(Timecode.Hours) { Print(Dest, "%i:%02i:%02i", Timecode.Hours, Timecode.Minutes, Timecode.Seconds); } else { Print(Dest, "%i:%02i", Timecode.Minutes, Timecode.Seconds); } Colourise(CS_END); } void IndexingChronologyError(string *Filename, uint64_t LineNumber, v4 ThisTimecode, v4 PrevTimecode) { severity Severity = S_ERROR; ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_INDEXING); Print(stderr, "Timecode "); PrintTimecode(stderr, ThisTimecode); Print(stderr, " is chronologically earlier than previous timecode ("); PrintTimecode(stderr, PrevTimecode); Print(stderr, ")\n"); } void IndexingQuoteError(string *Filename, uint64_t LineNumber, string Author, uint64_t QuoteID) { ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_INDEXING); Print(stderr, "Quote not found: "); Colourise(CS_MAGENTA_BOLD); Print(stderr, "#"); PrintString(Author); Print(stderr, " %ld", QuoteID); Colourise(CS_END); Print(stderr, "\n"); } void IndexingErrorSizing(string *Filename, uint64_t LineNumber, char *Key, string Received, uint64_t MaxSize) { ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_INDEXING); Print(stderr, "%s value is too long (%lu/%lu characters): ", Key, Received.Length, MaxSize); PrintStringC(CS_MAGENTA_BOLD, Received); Print(stderr, "\n"); } void IndexingErrorClash(string *Filename, uint64_t LineNumber, severity Severity, char *Key, string Received, string ExistingEntryBaseFilename) { ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_INDEXING); Print(stderr, "%s value %s%.*s%s clashes with that of existing entry: ", Key, ColourStrings[CS_MAGENTA_BOLD], (int)Received.Length, Received.Base, ColourStrings[CS_END]); PrintStringC(CS_MAGENTA, ExistingEntryBaseFilename); Print(stderr, "\n"); } void PrintValidLanguageCodeChars(void) { Print(stderr, " Valid characters:\n" " a to z\n" " A to Z\n" " 0 to 9\n" " - (hyphen)\n"); } void IndexingErrorInvalidLanguageCode(string *Filename, uint64_t LineNumber, char *Key, string Received) { ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_INDEXING); Print(stderr, "%s value %s%.*s%s contains invalid character(s)\n", Key, ColourStrings[CS_MAGENTA_BOLD], (int)Received.Length, Received.Base, ColourStrings[CS_END]); } void IndexingErrorEmptyField(string *Filename, uint64_t LineNumber, char *Key) { ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_INDEXING); Print(stderr, "%s field cannot be empty\n", Key); } void IndexingErrorInvalidSubstring(string *Filename, uint64_t LineNumber, char *Key, string Received, string InvalidSubstring) { ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_INDEXING); Print(stderr, "%s value %s%.*s%s contains invalid substring: ", Key, ColourStrings[CS_MAGENTA_BOLD], (int)Received.Length, Received.Base, ColourStrings[CS_END]); PrintStringC(CS_MAGENTA, InvalidSubstring); Print(stderr, "\n"); } void IndexingErrorCustomSizing(string *Filename, uint64_t LineNumber, int CustomIndex, string Received) { ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_INDEXING); Print(stderr, "custom%d value is too long (%lu/%i characters): ", CustomIndex, Received.Length, CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH); PrintStringC(CS_MAGENTA_BOLD, Received); Print(stderr, "\n"); if(Received.Length < MAX_CUSTOM_SNIPPET_LONG_LENGTH) { Print(stderr, "Consider using custom12 to custom15, which can hold %d characters\n", MAX_CUSTOM_SNIPPET_LONG_LENGTH); } else { Print(stderr, "Consider using a bespoke template for longer amounts of localised information\n"); } } void SystemError(string *Filename, uint64_t LineNumber, severity Severity, char *Message, string *Received) { ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_SYSTEM); // TODO(matt): Typeset the Message? Print(stderr, "%s", Message); if(Received) { PrintStringC(CS_MAGENTA_BOLD, *Received); } Print(stderr, "\n"); } typedef 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_COUNT, } log_level; char *LogLevelStrings[] = { "emergency", "alert", "critical", "error", "warning", "notice", "informational", "debug", }; log_level GetLogLevelFromString(string *Filename, token *T) { for(int i = 0; i < LOG_COUNT; ++i) { if(!StringsDifferLv0(T->Content, LogLevelStrings[i])) { return i; } } ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown log level: ", &T->Content); Print(stderr, " Valid log levels:\n"); for(int i = 0; i < LOG_COUNT; ++i) { Print(stderr, " %s\n", LogLevelStrings[i]); } return LOG_COUNT; } void WaitForInput(void); // NOTE(matt): Forward declared void FreeBuffer(buffer *Buffer) { /* */ MEM_TEST_TOP(); /* +MEM */ Free(Buffer->Location); /* */ MEM_TEST_MID(); Buffer->Ptr = 0; Buffer->Size = 0; //Buffer->ID = 0; Buffer->IndentLevel = 0; MEM_TEST_END(); } char *AssetTypeNames[] = { "Generic", "CSS", "Image", "JavaScript" }; typedef enum { ASSET_GENERIC, ASSET_CSS, ASSET_IMG, ASSET_JS, ASSET_TYPE_COUNT } asset_type; char *GenreStrings[] = { "video", }; typedef enum { GENRE_VIDEO, GENRE_COUNT, } genre; genre GetGenreFromString(string *Filename, token *T) { for(int i = 0; i < GENRE_COUNT; ++i) { if(!StringsDifferLv0(T->Content, GenreStrings[i])) { return i; } } ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown genre: ", &T->Content); Print(stderr, " Valid genres:\n"); for(int i = 0; i < GENRE_COUNT; ++i) { Print(stderr, " %s\n", GenreStrings[i]); } return GENRE_COUNT; } char *NumberingSchemeStrings[] = { "calendrical", "linear", "seasonal", }; typedef enum { NS_CALENDRICAL, NS_LINEAR, NS_SEASONAL, NS_COUNT, } numbering_scheme; numbering_scheme GetNumberingSchemeFromString(string *Filename, token *T) { for(int i = 0; i < NS_COUNT; ++i) { if(!StringsDifferLv0(T->Content, NumberingSchemeStrings[i])) { return i; } } ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown numbering scheme: ", &T->Content); Print(stderr, " Valid numbering schemes:\n"); for(int i = 0; i < NS_COUNT; ++i) { Print(stderr, " %s\n", NumberingSchemeStrings[i]); } return NS_COUNT; } char *NumberingMethodStrings[] = { "auto", "filename_derived", "hmml_specified", }; typedef enum { NM_AUTO, NM_FILENAME_DERIVED, NM_HMML_SPECIFIED, NM_COUNT } numbering_method; numbering_method GetNumberingMethodFromString(string *Filename, token *T) { for(int i = 0; i < NM_COUNT; ++i) { if(!StringsDifferLv0(T->Content, NumberingMethodStrings[i])) { return i; } } ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown numbering method: ", &T->Content); Print(stderr, " Valid numbering methods:\n"); for(int i = 0; i < NM_COUNT; ++i) { Print(stderr, " %s\n", NumberingMethodStrings[i]); } return NM_COUNT; } typedef struct { uint32_t StartingNumber; bool ZeroPadded; } numbering_auto_params; typedef struct { string Prefix; } numbering_filename_derived_params; typedef struct { string Unit; numbering_method Method; numbering_scheme Scheme; numbering_auto_params Auto; numbering_filename_derived_params FilenameDerived; } numbering; bool GetBoolFromString(string *Filename, token *T) { if(!StringsDifferLv0(T->Content, "true") || !StringsDifferLv0(T->Content, "True") || !StringsDifferLv0(T->Content, "TRUE") || !StringsDifferLv0(T->Content, "yes")) { return TRUE; } else if(!StringsDifferLv0(T->Content, "false") || !StringsDifferLv0(T->Content, "False") || !StringsDifferLv0(T->Content, "FALSE") || !StringsDifferLv0(T->Content, "no")) { return FALSE; } else { ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown boolean: ", &T->Content); Print(stderr, " Valid booleans:\n"); PrintC(CS_GREEN, " true\n"); PrintC(CS_GREEN, " True\n"); PrintC(CS_GREEN, " TRUE\n"); PrintC(CS_GREEN, " yes\n"); PrintC(CS_RED, " false\n"); PrintC(CS_RED, " False\n"); PrintC(CS_RED, " FALSE\n"); PrintC(CS_RED, " no\n"); return -1; } } /* // NOTE(matt): Sprite stuff art = "riscy_sprite.png"; art_variants = "light_normal light_focused light_disabled"; art_variants = "light"; art_variants = "dark_normal dark_focused dark_disabled"; art_variants = "dark"; art_variants = "light_normal dark_normal"; art_variants = "normal"; art_variants = "all"; //og_image = "*.png"; icon = ""; // all icon_type = "textual"; // or "graphical" icon_variants = "all"; // for "graphical" icons, same as art_variants icon_normal = ""; // fills the whole normal row icon_focused = ""; // fills the whole focused row icon_disabled = ""; // fills the whole disabled row // NOTE(matt): In the future, allow setting art and graphical icons in this manner, for Cinera itself to compose the sprite art_light_normal = "*.png"; art_light_focused = "*.png"; art_light_disabled = "*.png"; art_dark_normal = "*.png"; art_dark_focused = "*.png"; art_dark_disabled = "*.png"; icon_light_normal = "*.png"; icon_light_focused = "*.png"; icon_light_disabled = "*.png"; icon_dark_normal = "*.png"; icon_dark_focused = "*.png"; icon_dark_disabled = "*.png"; // // NOTE(matt): icon, with icon_type ("graphical" or "textual") // projects, media and support // if "graphical", one file with icon_variants // if "textual", one or many texts: icon_normal, icon_focused, icon_disabled // art, may only be graphical // projects and entries */ uint64_t GetArtVariantFromString(string S) { uint64_t Result = 0; for(int i = 0; i < CAV_COUNT; ++i) { config_art_variant *This = ConfigArtVariants + i; if(StringsMatch(S, Wrap0(This->String))) { Result = This->Mapping; } } return Result; } uint64_t SkipWhitespaceS(string *S, uint64_t Position) { while(Position < S->Length && S->Base[Position] && (S->Base[Position] == ' ' || S->Base[Position] == '\n' || S->Base[Position] == '\t')) { ++Position; } return Position; } bool IsValidIdentifierCharacter(char C) { return ((C >= 'a' && C <= 'z') || (C >= 'A' && C <= 'Z') || C == '_' || C == '-'); } bool IsValidLanguageCode(string S) { // TODO(matt): Add an upper limit to the length bool Result = TRUE; for(int i = 0; i < S.Length; ++i) { char C = S.Base[i]; if(!((C >= '0' && C <= '9') || (C >= 'a' && C <= 'z') || (C >= 'A' && C <= 'Z') || C == '-')) { Result = FALSE; break; } } return Result; } bool IsNumber(char C) { return (C >= '0' && C <= '9'); } int64_t ParseArtVariantsString(string *Filename, token *ArtVariantsString) { uint64_t Result = 0; bool Valid = TRUE; int i = SkipWhitespaceS(&ArtVariantsString->Content, 0); for(; i < ArtVariantsString->Content.Length; i = SkipWhitespaceS(&ArtVariantsString->Content, i)) { if(IsValidIdentifierCharacter(ArtVariantsString->Content.Base[i])) { string S = {}; S.Base = ArtVariantsString->Content.Base + i; for(; i < ArtVariantsString->Content.Length && IsValidIdentifierCharacter(ArtVariantsString->Content.Base[i]); ++i) { ++S.Length; } uint64_t Variant = GetArtVariantFromString(S); if(Variant != 0) { Result |= Variant; } else { ConfigError(Filename, ArtVariantsString->LineNumber, S_WARNING, "Unknown art_variant: ", &S); Valid = FALSE; } } } if(!Valid) { Print(stderr, " Valid variant_types:\n"); int FirstValidVariant = 1; for(int i = FirstValidVariant; i < CAV_COUNT; ++i) { Print(stderr, " %s\n", ConfigArtVariants[i].String); } Result = -1; } return Result; } char *IconTypeStrings[] = { 0, "graphical", "textual", }; typedef enum { IT_DEFAULT_UNSET, IT_GRAPHICAL, IT_TEXTUAL, IT_COUNT, } icon_type; icon_type GetIconTypeFromString(string *Filename, token *T) { for(int i = 0; i < IT_COUNT; ++i) { if(!StringsDifferLv0(T->Content, IconTypeStrings[i])) { return i; } } ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown icon_type: ", &T->Content); Print(stderr, " Valid icon_types:\n"); int FirstValidIconType = 1; for(int i = FirstValidIconType; i < IT_COUNT; ++i) { Print(stderr, " %s\n", IconTypeStrings[i]); } return IT_COUNT; } char *VODPlatformStrings[] = { 0, "direct", "vimeo", "youtube", }; typedef enum { VP_DEFAULT_UNSET, VP_DIRECT, VP_VIMEO, VP_YOUTUBE, VP_COUNT, } vod_platform; vod_platform GetVODPlatformFromString(string *Filename, token *T) { for(int i = 0; i < VP_COUNT; ++i) { if(!StringsDifferLv0(T->Content, VODPlatformStrings[i])) { return i; } } ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown vod_platform: ", &T->Content); Print(stderr, " Valid vod_platforms:\n"); int FirstValidVODPlatform = 1; for(int i = FirstValidVODPlatform; i < VP_COUNT; ++i) { Print(stderr, " %s\n", VODPlatformStrings[i]); } return VP_COUNT; } vod_platform GetVODPlatform(string VODPlatform) { for(int i = 0; i < VP_COUNT; ++i) { if(!StringsDifferLv0(VODPlatform, VODPlatformStrings[i])) { return i; } } return VP_COUNT; } bool IsValidVODPlatform(vod_platform V) { return V > VP_DEFAULT_UNSET && V < VP_COUNT; } // 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 File; file 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 File; file Metadata; db_header2 Header; db_entry2 Entry; } database2; // #define MAX_PLAYER_URL_PREFIX_LENGTH 16 // 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 { // NOTE(matt): PrevStart is Absolute (or relative to start of file), // the others are Relative to PrevStart unsigned int PrevStart, NextStart; // 8 unsigned short int PrevEnd, NextEnd; // 4 } link_insertion_offsets; // 12 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 File; file 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]; char ProjectName[MAX_PROJECT_NAME_LENGTH]; char BaseURL[MAX_BASE_URL_LENGTH]; char SearchLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH]; char PlayerLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH]; char PlayerURLPrefix[MAX_PLAYER_URL_PREFIX_LENGTH]; } db_header_entries4; typedef db_entry3 db_entry4; typedef struct { uint32_t BlockID; // 'ASET' uint16_t Count; char RootDir[MAX_ROOT_DIR_LENGTH]; char RootURL[MAX_ROOT_URL_LENGTH]; char CSSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH]; char ImagesDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH]; char JSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH]; } db_header_assets4; typedef struct { int32_t Hash; uint32_t LandmarkCount; enum8(asset_type) Type; char Filename[MAX_ASSET_FILENAME_LENGTH]; } db_asset4; typedef struct { int32_t EntryIndex; uint32_t Position; } db_landmark4; typedef struct { file File; file Metadata; db_header4 Header; db_header_entries4 EntriesHeader; db_entry4 Entry; db_header_assets4 AssetsHeader; db_asset4 Asset; db_landmark4 Landmark; } database4; // Database 5 typedef db_header4 db_header5; typedef struct { uint32_t BlockID; // 'PROJ' // NOTE(matt): This corresponds to the old NTRY block char GlobalSearchDir[MAX_BASE_DIR_LENGTH]; char GlobalSearchURL[MAX_BASE_URL_LENGTH]; uint64_t Count; char Reserved[244]; } db_block_projects5; char *SpecialAssetIndexStrings[] = { "SAI_TEXTUAL", "SAI_UNSET", }; typedef enum { SAI_TEXTUAL = -2, SAI_UNSET = -1, } special_asset_index; typedef struct { char ID[MAX_PROJECT_ID_LENGTH]; char Title[MAX_PROJECT_NAME_LENGTH]; char BaseDir[MAX_BASE_DIR_LENGTH]; char BaseURL[MAX_BASE_URL_LENGTH]; char SearchLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH]; char PlayerLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH]; char Theme[MAX_THEME_LENGTH]; char Unit[MAX_UNIT_LENGTH]; enum32(special_asset_index) ArtIndex; enum32(special_asset_index) IconIndex; uint64_t EntryCount; uint64_t ChildCount; char Reserved[24]; } db_header_project5; typedef struct { char HMMLBaseFilename[MAX_BASE_FILENAME_LENGTH]; char OutputLocation[MAX_ENTRY_OUTPUT_LENGTH]; char Title[MAX_TITLE_LENGTH]; unsigned short int Size; link_insertion_offsets LinkOffsets; enum32(special_asset_index) ArtIndex; char Number[MAX_NUMBER_LENGTH]; char Reserved[30]; } db_entry5; typedef struct db_block_assets5 { uint32_t BlockID; // 'ASET' uint16_t Count; char RootDir[MAX_ROOT_DIR_LENGTH]; char RootURL[MAX_ROOT_URL_LENGTH]; char CSSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH]; char ImagesDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH]; char JSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH]; char Reserved[154]; } db_block_assets5; typedef struct { char Filename[MAX_ASSET_FILENAME_LENGTH]; enum8(asset_type) Type; hash32 Hash; uint32_t LandmarkCount; uint64_t Associated:1; uint64_t Variants:63; uint32_t Width; uint32_t Height; char Reserved[39]; } db_asset5; // TODO(matt): Consider the required sizes of these people typedef struct { int32_t Generation; int32_t Index; } db_project_index5; typedef struct { db_project_index5 Project; int32_t Entry; } db_project_and_entry_index5; typedef struct { db_project_and_entry_index5 Index; uint32_t Position; } db_landmark5; typedef struct { file File; file_signposted Metadata; bool Ready; db_header5 Header; db_block_projects5 ProjectsBlock; db_header_project5 ProjectHeader; db_entry5 Entry; db_block_assets5 AssetsBlock; db_asset5 Asset; db_landmark5 Landmark; } database5; // // // #pragma pack(pop) #define CINERA_DB_VERSION 5 #define db_header db_header5 #define db_block_projects db_block_projects5 #define db_header_project db_header_project5 #define db_entry db_entry5 #define db_block_assets db_block_assets5 #define db_asset db_asset5 #define db_project_index db_project_index5 #define db_landmark db_landmark5 #define database database5 typedef enum { B_ASET, B_PROJ, } block_id; uint32_t GetFourFromBlockID(block_id ID) { uint32_t Result = 0; switch(ID) { case B_ASET: Result = FOURCC("ASET"); break; case B_PROJ: Result = FOURCC("PROJ"); break; } return Result; } // TODO(matt): Increment CINERA_DB_VERSION! typedef enum { DBS_BLOCK, DBS_ENTRY, DBS_HEADERED_SECTION, DBS_INDEXER_FIELD, } db_structure_item_type; typedef enum { IDX_SINGLE, IDX_COMPOUND, } db_structure_indexer_type; typedef struct { db_structure_item_type Type; void *Item; } db_structure_item; typedef struct { string Name; uint32_t Offset; uint32_t SizeOf; db_structure_item Parent; } db_structure_field; typedef struct { db_structure_indexer_type Type; db_structure_field Field; } db_structure_indexer; db_structure_field PackDBStructureField(string Name, uint32_t Offset, uint32_t SizeOf) { db_structure_field Result = { .Name = Name, .Offset = Offset, .SizeOf = SizeOf }; return Result; } typedef enum { DST_SELF, DST_CHUNK, } db_structure_sort_type; typedef struct { db_structure_field Field; db_structure_sort_type Type; } db_structure_sorting_field; typedef struct { string Name; uint32_t SizeOf; _memory_book(db_structure_indexer) IndexerFields; db_structure_item Parent; } db_structure_datatype; typedef struct { db_structure_datatype Self; db_structure_sorting_field SortingField; db_structure_field EntryCount; _memory_book(db_structure_item) IndexedIntoBy; db_structure_datatype Entry; db_structure_field ChildCount; db_structure_item Parent; } db_structure_headered_section; typedef struct { db_structure_headered_section HeaderedSection; db_structure_field Count; _memory_book(db_structure_item) IndexedIntoBy; uint32_t ID; string PrintableName; uint32_t SizeOf; db_structure_datatype Entry; } db_structure_block; typedef struct { uint32_t HexSignature; uint32_t SizeOf; db_structure_field BlockCount; } db_structure_header; typedef struct { _memory_book(db_structure_block) Blocks; db_structure_header Header; uint32_t Version; } db_structure; typedef struct { _memory_book(db_structure) Structure; memory_book Strings; } db_structures; db_structures DBStructures; db_structure *CurrentDBStructure; db_structure *PreviousDBStructure; db_structure_block * InitDBStructureBlock(db_structure *S, memory_book *Strings, char *ID, uint32_t SizeOf, char *CountName, uint32_t CountOffset, uint32_t CountSizeOf, char *EntryDataTypeName, uint32_t EntrySizeOf) { db_structure_block *Result = MakeSpaceInBook(&S->Blocks); Result->ID = FOURCC(ID); Result->PrintableName = WriteStringInBook(Strings, Wrap0(ID)); Result->SizeOf = SizeOf; Result->Count = PackDBStructureField(WriteStringInBook(Strings, Wrap0(CountName)), CountOffset, CountSizeOf); if(EntrySizeOf) { Result->Entry.Name = WriteStringInBook(Strings, Wrap0(EntryDataTypeName)); Result->Entry.SizeOf = EntrySizeOf; Result->Entry.Parent.Type = DBS_BLOCK; Result->Entry.Parent.Item = Result; Result->Entry.IndexerFields = InitBook(sizeof(db_structure_indexer), 4); } Result->IndexedIntoBy = InitBook(sizeof(db_structure_item), 4); return Result; } db_structure_headered_section * InitDBHeaderedSection(db_structure_block *B, memory_book *Strings, char *Name, uint32_t SizeOf, char *EntryCountName, uint32_t EntryCountOffset, uint32_t EntryCountSizeOf, char *ChildCountName, uint32_t ChildCountOffset, uint32_t ChildCountSizeOf, char *EntryDataTypeName, uint32_t EntrySizeOf) { db_structure_headered_section *Result = &B->HeaderedSection; Result->Self.Name = WriteStringInBook(Strings, Wrap0(Name)); Result->Self.SizeOf = SizeOf; if(EntryCountSizeOf) { Result->EntryCount = PackDBStructureField(WriteStringInBook(Strings, Wrap0(EntryCountName)), EntryCountOffset, EntryCountSizeOf); Result->Entry.Name = WriteStringInBook(Strings, Wrap0(EntryDataTypeName)); Result->Entry.SizeOf = EntrySizeOf; Result->Entry.Parent.Type = DBS_HEADERED_SECTION; Result->Entry.Parent.Item = Result; Result->Entry.IndexerFields = InitBook(sizeof(db_structure_indexer), 4); } if(ChildCountSizeOf) { Result->ChildCount = PackDBStructureField(WriteStringInBook(Strings, Wrap0(ChildCountName)), ChildCountOffset, ChildCountSizeOf); } Result->IndexedIntoBy = InitBook(sizeof(db_structure_item), 4); Result->Self.IndexerFields = InitBook(sizeof(db_structure_indexer), 4); Result->Parent.Type = DBS_BLOCK; Result->Parent.Item = B; return Result; } db_structure_indexer * DBStructureAddIndexerField(db_structure_datatype *D, memory_book *Strings, db_structure_item_type ParentType, char *Name, uint32_t Offset, uint32_t SizeOf, db_structure_indexer_type Type) { db_structure_indexer *Result = MakeSpaceInBook(&D->IndexerFields); Result->Field.Name = WriteStringInBook(Strings, Wrap0(Name)); Result->Field.Offset = Offset; Result->Field.SizeOf = SizeOf; Result->Type = Type; Result->Field.Parent.Type = ParentType; Result->Field.Parent.Item = D; return Result; } void BlockIsIndexedIntoBy(db_structure_block *B, db_structure_item_type Type, void *Indexer) { db_structure_item *New = MakeSpaceInBook(&B->IndexedIntoBy); New->Type = Type; New->Item = Indexer; } void HeaderedSectionIsIndexedIntoBy(db_structure_headered_section *H, db_structure_item_type Type, void *Indexer) { db_structure_item *New = MakeSpaceInBook(&H->IndexedIntoBy); New->Type = Type; New->Item = Indexer; } void InitDBStructure4(void) { db_structure *S = MakeSpaceInBook(&DBStructures.Structure); memory_book *Strings = &DBStructures.Strings; S->Version = 4; S->Header.HexSignature = FOURCC("CNRA"); S->Header.SizeOf = sizeof(db_header4); db_header4 db_header4_; S->Header.BlockCount = PackDBStructureField( WriteStringInBook(Strings, Wrap0("BlockCount")), offsetof(db_header4, BlockCount), sizeof(db_header4_.BlockCount)); S->Blocks = InitBook(sizeof(db_structure_block), 4); db_header_entries4 db_header_entries4_; db_structure_block *BlockNTRY = InitDBStructureBlock(S, Strings, "NTRY", sizeof(db_header_entries4_), "Count", offsetof(db_header_entries4, Count), sizeof(db_header_entries4_.Count), "db_entry4", sizeof(db_entry4)); db_header_assets4 db_header_assets4_; db_structure_block *BlockASET = InitDBStructureBlock(S, Strings, "ASET", sizeof(db_header_assets4_), "Count", offsetof(db_header_assets4, Count), sizeof(db_header_assets4_.Count), 0, 0); db_asset4 db_asset4_; db_structure_headered_section *HeaderAssets = InitDBHeaderedSection(BlockASET, Strings, "db_asset4", sizeof(db_asset4_), "LandmarkCount", offsetof(db_asset4, LandmarkCount), sizeof(db_asset4_.LandmarkCount), 0, 0, 0, "db_landmark4", sizeof(db_landmark4)); BlockIsIndexedIntoBy(BlockNTRY, DBS_ENTRY, &HeaderAssets->Entry); } db_structure * InitDBStructure5(void) { db_structure *S = MakeSpaceInBook(&DBStructures.Structure); memory_book *Strings = &DBStructures.Strings; S->Version = 5; S->Header.HexSignature = FOURCC("CNRA"); S->Header.SizeOf = sizeof(db_header5); db_header5 db_header5_; S->Header.BlockCount = PackDBStructureField( WriteStringInBook(Strings, Wrap0("BlockCount")), offsetof(db_header5, BlockCount), sizeof(db_header5_.BlockCount)); S->Blocks = InitBook(sizeof(db_structure_block), 4); db_block_projects5 db_block_projects5_; db_structure_block *BlockPROJ = InitDBStructureBlock(S, Strings, "PROJ", sizeof(db_block_projects5_), "Count", offsetof(db_block_projects5, Count), sizeof(db_block_projects5_.Count), 0, 0); db_header_project5 db_header_project5_; db_structure_headered_section *HeaderProjects = InitDBHeaderedSection(BlockPROJ, Strings, "db_header_project5", sizeof(db_header_project5_), "EntryCount", offsetof(db_header_project5, EntryCount), sizeof(db_header_project5_.EntryCount), "ChildCount", offsetof(db_header_project5, ChildCount), sizeof(db_header_project5_.ChildCount), "db_entry5", sizeof(db_entry5)); db_structure_indexer *ProjectsArtIndex = DBStructureAddIndexerField(&HeaderProjects->Self, Strings, DBS_HEADERED_SECTION, "ArtIndex", offsetof(db_header_project5, ArtIndex), sizeof(db_header_project5_.ArtIndex), IDX_SINGLE); db_structure_indexer *ProjectsIconIndex = DBStructureAddIndexerField(&HeaderProjects->Self, Strings, DBS_HEADERED_SECTION, "IconIndex", offsetof(db_header_project5, IconIndex), sizeof(db_header_project5_.IconIndex), IDX_SINGLE); db_entry5 db_entry5_; db_structure_indexer *EntryArtIndex = DBStructureAddIndexerField(&HeaderProjects->Entry, Strings, DBS_ENTRY, "ArtIndex", offsetof(db_entry5, ArtIndex), sizeof(db_entry5_.ArtIndex), IDX_SINGLE); db_block_assets5 db_block_assets5_; db_structure_block *BlockASET = InitDBStructureBlock(S, Strings, "ASET", sizeof(db_block_assets5_), "Count", offsetof(db_block_assets5, Count), sizeof(db_block_assets5_.Count), 0, 0); db_asset5 db_asset5_; db_structure_headered_section *HeaderAssets = InitDBHeaderedSection(BlockASET, Strings, "db_asset5", sizeof(db_asset5_), "LandmarkCount", offsetof(db_asset5, LandmarkCount), sizeof(db_asset5_.LandmarkCount), 0, 0, 0, "db_landmark5", sizeof(db_landmark5)); db_landmark5 db_landmark5_; db_structure_indexer *LandmarkProjectAndEntryIndex = DBStructureAddIndexerField(&HeaderAssets->Entry, Strings, DBS_ENTRY, "Index", offsetof(db_landmark5, Index), sizeof(db_landmark5_.Index), IDX_COMPOUND); BlockIsIndexedIntoBy(BlockPROJ, DBS_INDEXER_FIELD, LandmarkProjectAndEntryIndex); HeaderedSectionIsIndexedIntoBy(HeaderProjects, DBS_INDEXER_FIELD, LandmarkProjectAndEntryIndex); BlockIsIndexedIntoBy(BlockASET, DBS_INDEXER_FIELD, ProjectsArtIndex); BlockIsIndexedIntoBy(BlockASET, DBS_INDEXER_FIELD, ProjectsIconIndex); BlockIsIndexedIntoBy(BlockASET, DBS_INDEXER_FIELD, EntryArtIndex); return S; } void InitDBStructures(void) { DBStructures.Structure = InitBook(sizeof(db_structure), 4); DBStructures.Strings = InitBookOfStrings(Kilobytes(4)); InitDBStructure4(); CurrentDBStructure = InitDBStructure5(); } uint64_t ReadDBField(void *ParentInDB, db_structure_field F) { uint64_t Result = 0; char *Ptr = ParentInDB; Ptr += F.Offset; memcpy(&Result, Ptr, F.SizeOf); return Result; } db_structure_block * GetDBStructureBlock(uint32_t BlockID) { // NOTE(matt): Usage: To use an older database version, temporarily set CurrentDBStructure before calling this function db_structure_block *Result = 0; for(int i = 0; i < CurrentDBStructure->Blocks.ItemCount; ++i) { db_structure_block *This = GetPlaceInBook(&CurrentDBStructure->Blocks, i); if(This->ID == BlockID) { Result = This; break; } } return Result; } db_structure_headered_section * GetDBStructureHeaderedSection(block_id BlockID) { // NOTE(matt): Usage: To use an older database version, temporarily set CurrentDBStructure before calling this function return &GetDBStructureBlock(GetFourFromBlockID(BlockID))->HeaderedSection; } void PrintFour(FILE *Dest, uint32_t Four) { Colourise(CS_YELLOW); Print(Dest, "%c%c%c%c", Four, Four >> 8, Four >> 16, Four >> 24); Colourise(CS_END); } void PrintDBStructureDataType(FILE *Dest, db_structure_datatype DataType) { Print(Dest, "%s%.*s%s (%s%u%s byte%s)", ColourStrings[CS_MAGENTA], (int)DataType.Name.Length, DataType.Name.Base, ColourStrings[CS_END], ColourStrings[CS_BLUE_BOLD], DataType.SizeOf, ColourStrings[CS_END], DataType.SizeOf == 1 ? "" : "s"); } void TypesetDBStructureField(FILE *Dest, int IndentLevel, db_structure_field Field) { if(Field.SizeOf) { IndentedCarriageReturn(IndentLevel); Print(Dest, "%s%.*s%s: %s%u%s byte%s wide @ %s%u%s-byte offset", ColourStrings[CS_GREEN], (int)Field.Name.Length, Field.Name.Base, ColourStrings[CS_END], ColourStrings[CS_BLUE_BOLD], Field.SizeOf, ColourStrings[CS_END], Field.SizeOf == 1 ? "" : "s", ColourStrings[CS_BLUE_BOLD], Field.Offset, ColourStrings[CS_END]); } } void PrintDBBlockIDandSize(FILE *Dest, int IndentLevel, uint32_t ID, uint32_t SizeOf) { PrintFour(Dest, ID); Print(Dest, " (%s%u%s bytes)", ColourStrings[CS_BLUE_BOLD], SizeOf, ColourStrings[CS_END]); } void PrintDBStructureHeader(FILE *Dest, int IndentLevel, db_structure_header *Header) { IndentedCarriageReturn(IndentLevel); PrintDBBlockIDandSize(Dest, IndentLevel, Header->HexSignature, Header->SizeOf); ++IndentLevel; TypesetDBStructureField(Dest, IndentLevel, Header->BlockCount); --IndentLevel; } typedef struct { string *Name; db_structure_item_type StructuralItemType; db_structure_indexer_type IndexerType; } db_indexer_name_and_type; void PrintDBStructureItemLineage(FILE *Dest, db_structure_item *Item) { db_structure_item *This = Item; memory_book Lineage = InitBook(sizeof(db_indexer_name_and_type), 4); while(This) { db_indexer_name_and_type *New = MakeSpaceInBook(&Lineage); New->StructuralItemType = This->Type; switch(This->Type) { case DBS_BLOCK: { db_structure_block *It = (db_structure_block *)This->Item; New->Name = &It->PrintableName; This = 0; } break; case DBS_ENTRY: { db_structure_datatype *It = (db_structure_datatype *)This->Item; New->Name = &It->Name; This = &It->Parent; } break; case DBS_HEADERED_SECTION: { db_structure_headered_section *It = (db_structure_headered_section *)This->Item; New->Name = &It->Self.Name; This = &It->Parent; } break; case DBS_INDEXER_FIELD: { db_structure_indexer *It = (db_structure_indexer *)This->Item; New->Name = &It->Field.Name; New->IndexerType = It->Type; This = &It->Field.Parent; } break; } } db_indexer_name_and_type *Cursor = GetPlaceInBook(&Lineage, Lineage.ItemCount - 1); PrintString(*Cursor->Name); for(int i = Lineage.ItemCount - 2; i >= 0; --i) { Cursor = GetPlaceInBook(&Lineage, i); Print(Dest, "%s", Cursor->StructuralItemType == DBS_INDEXER_FIELD ? "." : "/"); PrintString(*Cursor->Name); if(Cursor->IndexerType == IDX_COMPOUND) { Print(Dest, " [compound]"); } } FreeBook(&Lineage); } void TypesetIndexers(FILE *Dest, int IndentLevel, memory_book *IndexedIntoBy) { if(IndexedIntoBy->ItemCount) { IndentedCarriageReturn(IndentLevel); Colourise(CS_YELLOW_BOLD); Print(Dest, "╿ is indexed into by:"); for(int i = 0; i < IndexedIntoBy->ItemCount - 1; ++i) { IndentedCarriageReturn(IndentLevel); db_structure_item *This = GetPlaceInBook(IndexedIntoBy, i); Print(Dest, "├── "); PrintDBStructureItemLineage(Dest, This); } IndentedCarriageReturn(IndentLevel); db_structure_item *This = GetPlaceInBook(IndexedIntoBy, IndexedIntoBy->ItemCount - 1); Print(Dest, "└── "); PrintDBStructureItemLineage(Dest, This); Colourise(CS_END); } } void PrintDBStructureHeaderedSection(FILE *Dest, int IndentLevel, db_structure_headered_section *H) { IndentedCarriageReturn(IndentLevel); PrintDBStructureDataType(Dest, H->Self); ++IndentLevel; TypesetDBStructureField(Dest, IndentLevel, H->EntryCount); TypesetIndexers(Dest, IndentLevel, &H->IndexedIntoBy); TypesetDBStructureField(Dest, IndentLevel, H->ChildCount); IndentedCarriageReturn(IndentLevel); PrintDBStructureDataType(Dest, H->Entry); --IndentLevel; } void PrintDBStructureBlock(FILE *Dest, int IndentLevel, db_structure_block *Block) { IndentedCarriageReturn(IndentLevel); PrintFour(Dest, Block->ID); Print(Dest, " (%s%u%s bytes)", ColourStrings[CS_BLUE_BOLD], Block->SizeOf, ColourStrings[CS_END]); ++IndentLevel; TypesetDBStructureField(Dest, IndentLevel, Block->Count); TypesetIndexers(Dest, IndentLevel, &Block->IndexedIntoBy); if(Block->HeaderedSection.Self.SizeOf) { PrintDBStructureHeaderedSection(Dest, IndentLevel, &Block->HeaderedSection); } if(Block->Entry.SizeOf) { IndentedCarriageReturn(IndentLevel); PrintDBStructureDataType(Dest, Block->Entry); } --IndentLevel; } void PrintDBStructure(FILE *Dest, int IndentLevel, db_structure *S) { IndentedCarriageReturn(IndentLevel); Print(Dest, "Version %s%u%s", ColourStrings[CS_MAGENTA_BOLD], S->Version, ColourStrings[CS_END]); PrintDBStructureHeader(Dest, IndentLevel, &S->Header); ++IndentLevel; for(int i = 0; i < S->Blocks.ItemCount; ++i) { db_structure_block *Block = GetPlaceInBook(&S->Blocks, i); PrintDBStructureBlock(Dest, IndentLevel, Block); } --IndentLevel; } void PrintDBStructures(FILE *Dest) { int IndentLevel = 0; for(int i = 0; i < DBStructures.Structure.ItemCount; ++i) { db_structure *S = GetPlaceInBook(&DBStructures.Structure, i); PrintDBStructure(Dest, IndentLevel, S); } Print(Dest, "\n"); } typedef struct { buffer_id BufferID; uint32_t Offset; } landmark; typedef struct { db_project_index ProjectIndex; config_identifier_id Type; // IDENT_ART or IDENT_ICON } asset_association; typedef struct { uint32_t Width; uint32_t Height; } vec2; typedef struct { vec2 TileDim; int32_t XLight; int32_t XDark; int32_t YNormal; int32_t YFocused; int32_t YDisabled; } sprite; typedef struct asset { asset_type Type; char Filename[MAX_ASSET_FILENAME_LENGTH]; asset_hash Hash; vec2 Dimensions; sprite Sprite; uint64_t Variants:63; uint64_t Associated:1; int32_t FilenameAt:29; int32_t Known:1; int32_t OffsetLandmarks:1; int32_t DeferredUpdate:1; _memory_book(landmark) Search; _memory_book(landmark) Player; } asset; asset BuiltinAssets[] = { { ASSET_CSS, "cinera.css" }, { ASSET_CSS, "cinera_topics.css" }, { ASSET_IMG, "cinera_icon_filter.png" }, { ASSET_JS, "cinera_pre.js" }, { ASSET_JS, "cinera_post.js" }, { ASSET_JS, "cinera_clear.js" }, { ASSET_JS, "cinera_search_pre.js" }, { ASSET_JS, "cinera_search_post.js" }, { ASSET_JS, "cinera_player_pre.js" }, { ASSET_JS, "cinera_player_post.js" }, }; typedef enum { ASSET_CSS_CINERA, ASSET_CSS_TOPICS, ASSET_IMG_FILTER, ASSET_JS_CINERA_PRE, ASSET_JS_CINERA_POST, ASSET_JS_CLEAR, ASSET_JS_SEARCH_PRE, ASSET_JS_SEARCH_POST, ASSET_JS_PLAYER_PRE, ASSET_JS_PLAYER_POST, BUILTIN_ASSETS_COUNT, } builtin_asset_id; typedef enum { // Contents and Player Pages Mandatory TAG_INCLUDES, // Contents Page Mandatory TAG_SEARCH, // Player Page Mandatory TAG_PLAYER, // 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_NAV, TAG_GLOBAL_NAV, // TAG_PATHED_NAV, TAG_PROJECT, TAG_PROJECT_ID, TAG_PROJECT_LINEAGE, TAG_PROJECT_PLAIN, TAG_SEARCH_URL, TAG_THEME, TAG_URL, TEMPLATE_TAG_COUNT, } template_tag_code; char *TemplateTags[] = { "__CINERA_INCLUDES__", "__CINERA_SEARCH__", "__CINERA_PLAYER__", "__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_NAV__", "__CINERA_GLOBAL_NAV__", //"__CINERA_PATHED_NAV__", "__CINERA_PROJECT__", "__CINERA_PROJECT_ID__", "__CINERA_PROJECT_LINEAGE__", "__CINERA_PROJECT_PLAIN__", "__CINERA_SEARCH_URL__", "__CINERA_THEME__", "__CINERA_URL__", }; char *NavigationTypes[] = { 0, "dropdown", "horizontal", "plain", }; typedef enum { NT_NULL, NT_DROPDOWN, NT_HORIZONTAL, NT_PLAIN, NT_COUNT, } navigation_type; typedef struct project project; // NOTE(matt): Forward declared. Consider reorganising the code? typedef struct { project *ChildrenOf; navigation_type Type; } navigation_spec; typedef struct { buffer Buffer; navigation_spec Spec; } navigation_buffer; typedef struct { int Offset; template_tag_code TagCode; asset *Asset; navigation_buffer *Nav; } tag_offset; typedef enum { TEMPLATE_NULL, TEMPLATE_GLOBAL_SEARCH, TEMPLATE_SEARCH, TEMPLATE_PLAYER, TEMPLATE_BESPOKE } template_type; typedef struct { int Validity; // NOTE(matt): Bitmask describing which page the template is valid for, i.e. contents and / or player page template_type Type; bool RequiresCineraJS; _memory_book(tag_offset) Tags; _memory_book(navigation_buffer) NavBuffer; } template_metadata; typedef struct { file File; template_metadata Metadata; } template; void FreeTemplateNavBuffers(template *T) { for(int i = 0; i < T->Metadata.NavBuffer.ItemCount; ++i) { navigation_buffer *This = GetPlaceInBook(&T->Metadata.NavBuffer, i); FreeBuffer(&This->Buffer); } } void ClearTemplateMetadata(template *Template) { FreeTemplateNavBuffers(Template); FreeBook(&Template->Metadata.NavBuffer); FreeBook(&Template->Metadata.Tags); template_metadata Zero = {}; Template->Metadata = Zero; } void FreeFileBufferAndPath(file *F) { FreeBuffer(&F->Buffer); Free(F->Path); } void FreeFile(file *F, bool CloseLocking) { CloseFile(F, CloseLocking); FreeFileBufferAndPath(F); } void FreeSignpostedFile(file_signposted *F, bool CloseLocking) { FreeFile(&F->File, CloseLocking); file_signposts Zero = {}; F->Signposts = Zero; } void FreeTemplate(template *Template) { FreeFile(&Template->File, NA); ClearTemplateMetadata(Template); } string StripComponentFromPath(string Path) { string Result = Path; while(Result.Length > 0 && Result.Base[Result.Length - 1] != '/') { --Result.Length; } if(Result.Length > 1) { --Result.Length; } return Result; } int StripComponentFromPath0(char *Path) { // TODO(matt): Beware that this may leak, perhaps? I dunno. char *Ptr = Path + StringLength(Path) - 1; if(Ptr < Path) { return RC_ERROR_DIRECTORY; } while(Ptr > Path && *Ptr != '/') { --Ptr; } *Ptr = '\0'; return RC_SUCCESS; } int FinalPathComponentPosition(string Path) { char *Ptr = Path.Base + Path.Length - 1; while(Ptr > Path.Base && *Ptr != '/') { --Ptr; } if(*Ptr == '/') { ++Ptr; } return Ptr - Path.Base; } string GetFinalComponent(string Path) { int NewPos = FinalPathComponentPosition(Path); string Result = { .Base = Path.Base + NewPos, .Length = Path.Length - NewPos }; return Result; } string GetBaseFilename(string Filepath, extension_id Extension // Pass EXT_NULL (or 0) to retain the whole file path, only without its parent directories ) { string Result = GetFinalComponent(Filepath); if(Extension != EXT_NULL) { Result.Length -= ExtensionStrings[Extension].Length; } return Result; } string TrimString(string S, uint32_t CharsFromStart, uint32_t CharsFromEnd) { string Result = S; int OriginalLength = Result.Length; Result.Length -= MIN(OriginalLength, (CharsFromStart + CharsFromEnd)); Result.Base += MIN(OriginalLength, CharsFromStart); return Result; } string TrimWhitespace(string S) { string Result = S; while(Result.Length > 0 && *Result.Base == ' ') { ++Result.Base; --Result.Length; } while(Result.Length > 0 && Result.Base[Result.Length - 1] == ' ') { --Result.Length; } return Result; } char *WatchTypeStrings[] = { "WT_HMML", "WT_ASSET", "WT_CONFIG", }; typedef enum { WT_HMML, WT_ASSET, WT_CONFIG, } watch_type; typedef struct { watch_type Type; string Path; extension_id Extension; project *Project; asset *Asset; FILE *Handle; } watch_file; typedef struct { int Descriptor; string WatchedPath; string TargetPath; _memory_book(watch_file) Files; } watch_handle; typedef struct { _memory_book(watch_handle) Handles; memory_book Paths; uint32_t DefaultEventsMask; } watch_handles; #include "cinera_config.c" #define AFD 1 #define AFE 0 // NOTE(matt): Globals bool GlobalRunning; config *Config; project *CurrentProject; mode Mode; arena MemoryArena; _memory_book(asset) Assets; int inotifyInstance; watch_handles WatchHandles; database DB; time_t LastPrivacyCheck; time_t LastQuoteFetch; #define GLOBAL_UPDATE_INTERVAL 1 // void WaitForInput() { if(!Config || !Config->SuppressingPrompts) { Print(stderr, "Press Enter to continue...\n"); getchar(); } } bool SeekConfirmation(char *Message, bool Default) { bool Result = Default; if(!Config || !Config->SuppressingPrompts) { Print(stderr, "%s (%s)\n", Message, Default == TRUE ? "Y/n" : "y/N"); switch(getchar()) { case 'n': case 'N': Result = FALSE; break; case 'y': case 'Y': Result = TRUE; break; default: break; }; } return Result; } typedef struct { db_header_project *Project; db_entry *Prev; db_entry *This; db_entry *Next; db_entry WorkingThis; uint32_t PreLinkPrevOffsetTotal, PreLinkThisOffsetTotal, PreLinkNextOffsetTotal; uint32_t PrevOffsetModifier, ThisOffsetModifier, NextOffsetModifier; bool FormerIsFirst, LatterIsFinal; bool DeletedEntryWasFirst, DeletedEntryWasFinal; int16_t PrevIndex, PreDeletionThisIndex, ThisIndex, NextIndex; } neighbourhood; typedef struct { char *Name; colour_code Colour; } edit_type; typedef enum { EDIT_INSERTION, EDIT_APPEND, EDIT_REINSERTION, EDIT_DELETION, EDIT_ADDITION, EDIT_SKIP, } edit_type_id; edit_type EditTypes[] = { { "Inserted", CS_ADDITION }, { "Appended", CS_ADDITION }, { "Reinserted", CS_REINSERTION }, { "Deleted", CS_DELETION }, { "Added", CS_ADDITION }, { "Skipped", CS_ERROR }, }; void LogUsage(buffer *Buffer) { #if DEBUG // NOTE(matt): Stack-string char LogPath[256]; CopyString(LogPath, "%s/%s", CurrentProject->CacheDir, "buffers.log"); FILE *LogFile; if(!(LogFile = fopen(LogPath, "a+"))) { MakeDir(CurrentProject->CacheDir); if(!(LogFile = fopen(LogPath, "a+"))) { perror("LogUsage"); return; } } WriteToFile(LogFile, "%s,%ld,%d\n", Buffer->ID, Buffer->Ptr - Buffer->Location, Buffer->Size); fclose(LogFile); #endif } bool MakeDir(string Path); __attribute__ ((format (printf, 2, 3))) void LogError(int LogLevel, char *Format, ...) { if(Config->LogLevel >= LogLevel) { char *LogPath = MakeString0("ls", &Config->CacheDir, "/errors.log"); FILE *LogFile; if(!(LogFile = fopen(LogPath, "a+"))) { MakeDir(Config->CacheDir); if(!(LogFile = fopen(LogPath, "a+"))) { perror("LogUsage"); Free(LogPath); 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 WriteToFile(LogFile, "\n"); fclose(LogFile); Free(LogPath); } } void LogEdit(edit_type_id EditType, string Lineage, string EntryID, string *EntryTitle, bool Private) { if(Private) { LogError(LOG_NOTICE, "Privately %s %.*s/%.*s", EditTypes[EditType].Name, (int)Lineage.Length, Lineage.Base, (int)EntryID.Length, EntryID.Base); } else { LogError(LOG_NOTICE, "%s %.*s/%.*s - %.*s", EditTypes[EditType].Name, (int)Lineage.Length, Lineage.Base, (int)EntryID.Length, EntryID.Base, (int)EntryTitle->Length, EntryTitle->Base); } } #define CINERA_HSL_TRANSPARENT_HUE 65535 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, string String) { Colour->Hue = 0; Colour->Saturation = 0; Colour->Lightness = 74; for(int i = 0; i < String.Length; ++i) { Colour->Hue += CharToColour(String.Base[i]).Hue; Colour->Saturation += CharToColour(String.Base[i]).Saturation; } Colour->Hue = Colour->Hue % 360; Colour->Saturation = Colour->Saturation % 26 + 74; } bool IsValidHSLColour(hsl_colour Colour) { return (Colour.Hue >= 0 && Colour.Hue < 360) && (Colour.Saturation >= 0 && Colour.Saturation <= 100) && (Colour.Lightness >= 0 && Colour.Lightness <= 100); } #define SLASH 1 #define NULLTERM 1 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 Player; char Custom0[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM]; char Custom1[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM]; char Custom2[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM]; char Custom3[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM]; char Custom4[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM]; char Custom5[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM]; char Custom6[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM]; char Custom7[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM]; char Custom8[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM]; char Custom9[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM]; char Custom10[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM]; char Custom11[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM]; char Custom12[MAX_CUSTOM_SNIPPET_LONG_LENGTH + NULLTERM]; char Custom13[MAX_CUSTOM_SNIPPET_LONG_LENGTH + NULLTERM]; char Custom14[MAX_CUSTOM_SNIPPET_LONG_LENGTH + NULLTERM]; char Custom15[MAX_CUSTOM_SNIPPET_LONG_LENGTH + NULLTERM]; char Title[MAX_TITLE_LENGTH + NULLTERM]; char URLSearch[MAX_BASE_URL_LENGTH + SLASH + MAX_RELATIVE_PAGE_LOCATION_LENGTH + NULLTERM]; char URLPlayer[MAX_BASE_URL_LENGTH + SLASH + MAX_RELATIVE_PAGE_LOCATION_LENGTH + SLASH + MAX_BASE_FILENAME_LENGTH + NULLTERM]; char VideoID[MAX_VOD_ID_LENGTH + NULLTERM]; vod_platform VODPlatform; } buffers; rc ClaimBuffer(buffer *Buffer, buffer_id 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; Print(stdout, " ClaimBuffer(%s): %ld\n" " Total ClaimedMemory: %ld (%.2f%%, leaving %ld free)\n\n", BufferIDStrings[Buffer->ID], Buffer->Size, MemoryArena.Ptr - MemoryArena.Location, PercentageUsed, MemoryArena.Size - (MemoryArena.Ptr - MemoryArena.Location)); #endif return RC_SUCCESS; } void DeclaimBuffer(buffer *Buffer) { if(Buffer->Size > 0) { float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100; #if DEBUG Print(stdout, "DeclaimBuffer(%s)\n" " Used: %li / %ld (%.2f%%)\n" "\n" " Total ClaimedMemory: %ld\n\n", BufferIDStrings[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 admin@miblo.net LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", BufferIDStrings[Buffer->ID], PercentageUsed); Print(stderr, "%sWarning%s: %s used %.2f%% of its allotted memory\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], BufferIDStrings[Buffer->ID], PercentageUsed); } else if(PercentageUsed >= 80.0f) { // TODO(matt): Implement either dynamically growing buffers, or phoning home to admin@miblo.net LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", BufferIDStrings[Buffer->ID], PercentageUsed); Print(stderr, "%sWarning%s: %s used %.2f%% of its allotted memory\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], BufferIDStrings[Buffer->ID], PercentageUsed); } *Buffer->Location = '\0'; Buffer->Ptr = Buffer->Location; MemoryArena.Ptr -= Buffer->Size; Buffer->Size = 0; //Buffer->ID = 0; } } void RewindBuffer(buffer *Buffer) { #if DEBUG float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100; Print(stdout, "Rewinding %s\n" " Used: %ld / %ld (%.2f%%)\n\n", BufferIDStrings[Buffer->ID], Buffer->Ptr - Buffer->Location, Buffer->Size, PercentageUsed); #endif Buffer->Ptr = Buffer->Location; } void RewindCollationBuffers(buffers *CollationBuffers) { RewindBuffer(&CollationBuffers->IncludesPlayer); RewindBuffer(&CollationBuffers->Player); RewindBuffer(&CollationBuffers->IncludesSearch); RewindBuffer(&CollationBuffers->SearchEntry); } // NOTE(matt): Special-purposes indices, for project generation-index and entry index db_project_index GLOBAL_SEARCH_PAGE_INDEX = { -1, 0 }; typedef enum { SP_SEARCH = -1, } special_page_id; typedef struct { uint32_t Date; string Text; } quote_info; typedef struct { v4 Timecode; int Identifier; } identifier; typedef struct { string RefTitle; string URL; string Source; char *ID; _memory_book(identifier) Identifier; } ref_info; typedef struct { string Marker; string WrittenText; hsl_colour Colour; } category_info; #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) { Print(stdout, "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 ClearCopyString(Dest, DestSize, Format, ...) ClearCopyString_(__LINE__, (Dest), (DestSize), (Format), ##__VA_ARGS__) __attribute__ ((format (printf, 4, 5))) int ClearCopyString_(int LineNumber, char Dest[], int DestSize, char *Format, ...) { Clear(Dest, DestSize); int Length = 0; va_list Args; va_start(Args, Format); Length = vsnprintf(Dest, DestSize, Format, Args); if(Length >= DestSize) { Print(stdout, "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, string String) { if(String.Length + 1 > DestSize) { Print(stdout, "CopyStringNoFormat() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %ld(+1)-character string:\n" "%.*s\n", LineNumber, DestSize, String.Length, (int)String.Length, String.Base); __asm__("int3"); } Dest += CopyBytes(Dest, String.Base, String.Length); *Dest = '\0'; return String.Length; } #define ClearCopyStringNoFormatOrTerminate(Dest, DestSize, String) ClearCopyStringNoFormatOrTerminate_(__LINE__, Dest, DestSize, String) int ClearCopyStringNoFormatOrTerminate_(int LineNumber, char *Dest, int DestSize, string String) { if(String.Length > DestSize) { Print(stdout, "ClearCopyStringNoFormatOrTerminate() call on line %d has been passed a buffer too small (%d bytes) to contain %ld-character string:\n" "%.*s\n", LineNumber, DestSize, String.Length, (int)String.Length, String.Base); __asm__("int3"); } Clear(Dest, DestSize); return CopyBytes(Dest, String.Base, String.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 BytesToWrite = 0; while(String[BytesToWrite] && String[BytesToWrite] != Terminator) { ++BytesToWrite; } if(BytesToWrite >= DestSize) { Print(stdout, "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, BytesToWrite, BytesToWrite, String); __asm__("int3"); } Dest += CopyBytes(Dest, String, BytesToWrite); *Dest = '\0'; return BytesToWrite; } #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) { Print(stderr, "CopyStringToBuffer(%s) call on line %d cannot accommodate null-terminated %d(+1)-character string:\n" "%s\n", BufferIDStrings[Dest->ID], LineNumber, Length, Format); __asm__("int3"); } Dest->Ptr += Length; } int DigitsInTimecode(v4 Timecode) { int Result = 0; int ColonChar = 1; if(Timecode.Hours > 0) { Result += DigitsInInt(&Timecode.Hours); Result += ColonChar; Result += MAX(2, DigitsInInt(&Timecode.Minutes)); Result += ColonChar; Result += MAX(2, DigitsInInt(&Timecode.Seconds)); } else { Result += DigitsInInt(&Timecode.Minutes); Result += ColonChar; Result += MAX(2, DigitsInInt(&Timecode.Seconds)); } return Result; } #define CopyTimecodeToBuffer(Dest, Timecode) CopyTimecodeToBuffer_(__LINE__, Dest, Timecode) void CopyTimecodeToBuffer_(int LineNumber, buffer *Dest, v4 Timecode) { if(DigitsInTimecode(Timecode) + (Dest->Ptr - Dest->Location) >= Dest->Size) { Print(stderr, "CopyTimecodeToBuffer(%s) call on line %d cannot accommodate timecode:\n", BufferIDStrings[Dest->ID], LineNumber); PrintTimecode(stderr, Timecode); Print(stderr, "\n"); __asm__("int3"); } else { if(Timecode.Hours > 0) { CopyStringToBuffer(Dest, "%i:%02i:%02i", Timecode.Hours, Timecode.Minutes, Timecode.Seconds); } else { CopyStringToBuffer(Dest, "%i:%02i", Timecode.Minutes, Timecode.Seconds); } } } #define CopyStringToBufferNoFormat(Dest, String) CopyStringToBufferNoFormat_(__LINE__, Dest, String) void CopyStringToBufferNoFormat_(int LineNumber, buffer *Dest, string String) { if(String.Length + 1 > Dest->Size - (Dest->Ptr - Dest->Location)) { Print(stderr, "CopyStringToBufferNoFormat(%s) call on line %d cannot accommodate %ld-character string:\n" "%.*s\n", BufferIDStrings[Dest->ID], LineNumber, String.Length, (int)String.Length, String.Base); __asm__("int3"); } Dest->Ptr += CopyBytes(Dest->Ptr, String.Base, String.Length); *Dest->Ptr = '\0'; } #define CopyStringToBufferNoTerminate(Dest, Src) CopyStringToBufferNoTerminate_(__LINE__, Dest, Src) void CopyStringToBufferNoTerminate_(int LineNumber, buffer *Dest, string Src) { if((Dest->Ptr - Dest->Location + Src.Length) > Dest->Size) { Print(stderr, "CopyStringToBufferNoTerminate(%s) call on line %d cannot accommodate %ld-character string:\n" "%.*s\n", BufferIDStrings[Dest->ID], LineNumber, Src.Length, (int)Src.Length, Src.Base); __asm__("int3"); } Dest->Ptr += CopyBytes(Dest->Ptr, Src.Base, Src.Length); } #define CopyStringToBufferNoFormatL(Dest, Length, String) CopyStringToBufferNoFormatL_(__LINE__, Dest, Length, String) void CopyStringToBufferNoFormatL_(int LineNumber, buffer *Dest, int Length, char *String) { if(Dest->Ptr - Dest->Location + Length + 1 >= Dest->Size) { Print(stderr, "CopyStringToBufferNoFormat(%s) call on line %d cannot accommodate %ld(+1)-character string:\n" "%s\n", BufferIDStrings[Dest->ID], LineNumber, StringLength(String), String); __asm__("int3"); } Dest->Ptr += CopyBytes(Dest->Ptr, String, Length); *Dest->Ptr = '\0'; } #define CopyStringToBufferHTMLSafe(Dest, String) CopyStringToBufferHTMLSafe_(__LINE__, Dest, String) void CopyStringToBufferHTMLSafe_(int LineNumber, buffer *Dest, string String) { int Length = String.Length; for(int i = 0; i < String.Length; ++i) { switch(String.Base[i]) { case '<': case '>': Length += 3; break; case '&': case '\'': Length += 4; break; case '\"': Length += 5; break; default: break; } } if((Dest->Ptr - Dest->Location) + Length >= Dest->Size) { Print(stderr, "CopyStringToBufferHTMLSafe(%s) call on line %d cannot accommodate %d(+1)-character HTML-sanitised string:\n" "%.*s\n", BufferIDStrings[Dest->ID], LineNumber, Length, (int)String.Length, String.Base); __asm__("int3"); } for(int i = 0; i < String.Length; ++i) { switch(String.Base[i]) { 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.Base[i]; break; } } *Dest->Ptr = '\0'; } #define CopyStringToBufferHTMLSafeSurrounded(Dest, Pre, String, Post) CopyStringToBufferHTMLSafeSurrounded_(__LINE__, Dest, Pre, String, Post) void CopyStringToBufferHTMLSafeSurrounded_(int LineNumber, buffer *Dest, string Pre, string String, string Post) { // NOTE(matt): Only the String gets HTML-encoded. The Pre and Post parts get copied as-is CopyStringToBufferNoFormat_(LineNumber, Dest, Pre); CopyStringToBufferHTMLSafe_(LineNumber, Dest, String); CopyStringToBufferNoFormat_(LineNumber, Dest, Post); } #define CopyStringToBufferHTMLSafeBreakingOnSlash(Dest, String) CopyStringToBufferHTMLSafeBreakingOnSlash_(__LINE__, Dest, String) void CopyStringToBufferHTMLSafeBreakingOnSlash_(int LineNumber, buffer *Dest, string String) { int Length = String.Length; for(int i = 0; i < String.Length; ++i) { switch(String.Base[i]) { 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.Base[i]; break; } } if(Dest->Ptr - Dest->Location >= Dest->Size) { Print(stderr, "CopyStringToBufferHTMLSafeBreakingOnSlash(%s) call on line %d cannot accommodate %d(+1)-character HTML-sanitised string:\n" "%.*s\n", BufferIDStrings[Dest->ID], LineNumber, Length, (int)String.Length, String.Base); __asm__("int3"); } *Dest->Ptr = '\0'; } #define CopyStringToBufferHTMLPercentEncoded(Dest, String) CopyStringToBufferHTMLPercentEncoded_(__LINE__, Dest, String) void CopyStringToBufferHTMLPercentEncoded_(int LineNumber, buffer *Dest, string String) { int Length = String.Length; for(int i = 0; i < String.Length; ++ i) { switch(String.Base[i]) { case ' ': case '\"': case '%': case '&': case '<': case '>': case '?': case '\\': case '^': case '`': case '{': case '|': case '}': Length += 2; break; default: break; } } if((Dest->Ptr - Dest->Location) + Length >= Dest->Size) { Print(stderr, "CopyStringToBufferHTMLPercentEncodedL(%s) call on line %d cannot accommodate %d(+1)-character percent-encoded string:\n" "%.*s\n", BufferIDStrings[Dest->ID], LineNumber, Length, (int)String.Length, String.Base); __asm__("int3"); } for(int i = 0; i < String.Length; ++ i) { switch(String.Base[i]) { 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.Base[i]; break; } } *Dest->Ptr = '\0'; } #define CopyBuffer(Dest, Src) CopyBuffer_(__LINE__, Dest, Src) void CopyBuffer_(int LineNumber, buffer *Dest, buffer *Src) { if((Dest->Ptr - Dest->Location + Src->Ptr - Src->Location) >= Dest->Size) { Print(stderr, "CopyBuffer(%s) call on line %d cannot accommodate %ld(+1)-character %s\n", BufferIDStrings[Dest->ID], LineNumber, StringLength(Src->Location), BufferIDStrings[Src->ID]); __asm__("int3"); } Dest->Ptr += CopyBytes(Dest->Ptr, Src->Location, Src->Ptr - Src->Location); *Dest->Ptr = '\0'; } // NOTE(matt): Clash Detection // char *ResolutionStatusStrings[] = { "RS_NOT_STARTED", " RS_SEEN", " RS_PENDING", " RS_RESOLVED", " RS_REPORTED", }; typedef enum { RS_NOT_STARTED, // Earliest RS_SEEN, RS_PENDING, RS_RESOLVED, RS_REPORTED, // Latest } resolution_status; typedef struct { project *Project; char CandidateBaseFilename[MAX_BASE_FILENAME_LENGTH]; char CurrentOutputValue[MAX_ENTRY_OUTPUT_LENGTH]; char DesiredOutputValue[MAX_ENTRY_OUTPUT_LENGTH]; char IncumbentBaseFilename[MAX_BASE_FILENAME_LENGTH]; resolution_status Status; bool Vacating; bool Superseding; } clash_entry; char *ChainStructureStrings[] = { "Closed Loop", "Open Ended", }; typedef enum { CS_CLOSED_LOOP, CS_OPEN_ENDED } chain_structure; typedef struct { memory_book Book[2]; memory_book *Main; memory_book *Holder; memory_book Chain; chain_structure ChainStructure; bool ChainIsResolvable; bool Resolving; } clash_resolver; void InitClashResolver(clash_resolver *ClashResolver) { ClashResolver->Book[0] = InitBook(sizeof(clash_entry), 8); ClashResolver->Book[1] = InitBook(sizeof(clash_entry), 8); ClashResolver->Main = &ClashResolver->Book[0]; ClashResolver->Holder = &ClashResolver->Book[1]; ClashResolver->Chain = InitBookOfPointers(8); ClashResolver->ChainStructure = CS_OPEN_ENDED; ClashResolver->Resolving = FALSE; } void ResetClashResolver(clash_resolver *R) { FreeAndReinitialiseBook(&R->Book[0]); FreeAndReinitialiseBook(&R->Book[1]); R->Main = &R->Book[0]; R->Holder = &R->Book[1]; FreeAndReinitialiseBook(&R->Chain); R->ChainStructure = CS_OPEN_ENDED; R->Resolving = FALSE; } clash_entry * GetClash(memory_book *C, project *Project, string CandidateBaseFilename) { clash_entry *Result = 0; for(int i = 0; i < C->ItemCount; ++i) { clash_entry *This = GetPlaceInBook(C, i); if(Project == This->Project && StringsMatch(CandidateBaseFilename, Wrap0i(This->CandidateBaseFilename))) { Result = This; break; } } return Result; } clash_entry * GetFellowCandidateOf(memory_book *C, clash_entry *Candidate) { clash_entry *Result = 0; string IncumbentBaseFilename = Wrap0i(Candidate->IncumbentBaseFilename); for(int i = 0; i < C->ItemCount; ++i) { clash_entry *Fellow = GetPlaceInBook(C, i); if(Candidate != Fellow && Candidate->Project == Fellow->Project && (Fellow->Status < RS_PENDING || Fellow->Status == RS_REPORTED) && StringsMatch(IncumbentBaseFilename, Wrap0i(Fellow->IncumbentBaseFilename))) { Result = Fellow; break; } } return Result; } clash_entry * GetClashCandidateOf(memory_book *C, clash_entry *Incumbent) { clash_entry *Result = 0; string CurrentOutputValue = Wrap0i(Incumbent->CurrentOutputValue); for(int i = 0; i < C->ItemCount; ++i) { clash_entry *Candidate = GetPlaceInBook(C, i); if(Incumbent != Candidate && Candidate->Project == Incumbent->Project && !Candidate->Vacating && (Candidate->Status < RS_PENDING || Candidate->Status == RS_REPORTED) && StringsMatch(CurrentOutputValue, Wrap0i(Candidate->DesiredOutputValue))) { Result = Candidate; break; } } return Result; } clash_entry * GetClashIncumbentOf(memory_book *C, clash_entry *Candidate) { clash_entry *Result = 0; string IncumbentBaseFilename = Wrap0i(Candidate->IncumbentBaseFilename); for(int i = 0; i < C->ItemCount; ++i) { clash_entry *Incumbent = GetPlaceInBook(C, i); if(Candidate != Incumbent && Candidate->Project == Incumbent->Project && (Incumbent->Status < RS_PENDING || Incumbent->Status == RS_REPORTED) && StringsMatch(IncumbentBaseFilename, Wrap0i(Incumbent->CandidateBaseFilename))) { Result = Incumbent; break; } } return Result; } typedef enum { CR_FELLOW, CR_INCUMBENT, } clash_relationship; typedef struct { clash_relationship Relationship; clash_entry *Entry; } related_clash_entry; related_clash_entry GetFellowOrClashIncumbentOf(memory_book *C, clash_entry *Candidate) { related_clash_entry Result = {}; Result.Entry = GetFellowCandidateOf(C, Candidate); if(!Result.Entry) { Result.Entry = GetClashIncumbentOf(C, Candidate); Result.Relationship = CR_INCUMBENT; } return Result; } void AbsolveIncumbentOf(memory_book *C, clash_entry *Candidate) { clash_entry *Incumbent = GetClashIncumbentOf(C, Candidate); if(Incumbent) { Incumbent->Status = RS_NOT_STARTED; if(!GetFellowCandidateOf(C, Candidate) && Wrap0i(Incumbent->IncumbentBaseFilename).Length == 0) { Incumbent->Status = RS_RESOLVED; } Clear(Candidate->IncumbentBaseFilename, sizeof(Candidate->IncumbentBaseFilename)); } } void VacatePotentialClash(memory_book *C, project *Project, string OutgoingIncumbentBaseFilename) { clash_entry *OutgoingIncumbent = GetClash(C, Project, OutgoingIncumbentBaseFilename); if(OutgoingIncumbent) { AbsolveIncumbentOf(C, OutgoingIncumbent); OutgoingIncumbent->Vacating = TRUE; } } void ResolveClash(memory_book *C, project *Project, string BaseFilename) { clash_entry *This = GetClash(C, Project, BaseFilename); if(This) { AbsolveIncumbentOf(C, This); if(!GetClashCandidateOf(C, This)) { This->Status = RS_RESOLVED; } else { This->Status = RS_NOT_STARTED; } } } void PushClash(memory_book *C, project *Project, db_entry *IncumbentEntry, string CandidateBaseFilename, string CurrentOutputValue, string DesiredOutputValue) { clash_entry *This = GetClash(C, Project, CandidateBaseFilename); if(!This) { This = MakeSpaceInBook(C); } else if(StringsDiffer(Wrap0i(This->DesiredOutputValue), DesiredOutputValue)) { AbsolveIncumbentOf(C, This); } This->Project = Project; ClearCopyStringNoFormatOrTerminate(This->CandidateBaseFilename, sizeof(This->CandidateBaseFilename), CandidateBaseFilename); ClearCopyStringNoFormatOrTerminate(This->CurrentOutputValue, sizeof(This->CurrentOutputValue), CurrentOutputValue); ClearCopyStringNoFormatOrTerminate(This->DesiredOutputValue, sizeof(This->DesiredOutputValue), DesiredOutputValue); ClearCopyStringNoFormatOrTerminate(This->IncumbentBaseFilename, sizeof(This->IncumbentBaseFilename), Wrap0i(IncumbentEntry->HMMLBaseFilename)); This->Status = RS_NOT_STARTED; if(IncumbentEntry) { string IncumbentBaseFilename = Wrap0i(IncumbentEntry->HMMLBaseFilename); clash_entry *Incumbent = GetClash(C, Project, IncumbentBaseFilename); if(!Incumbent) { Incumbent = MakeSpaceInBook(C); Incumbent->Project = Project; ClearCopyStringNoFormatOrTerminate(Incumbent->CandidateBaseFilename, sizeof(Incumbent->CandidateBaseFilename), IncumbentBaseFilename); string IncumbentOutput = Wrap0i(IncumbentEntry->OutputLocation); ClearCopyStringNoFormatOrTerminate(Incumbent->CurrentOutputValue, sizeof(Incumbent->CurrentOutputValue), IncumbentOutput); ClearCopyStringNoFormatOrTerminate(Incumbent->DesiredOutputValue, sizeof(Incumbent->DesiredOutputValue), IncumbentOutput); } else if(Incumbent->Status != RS_REPORTED) { Incumbent->Status = RS_NOT_STARTED; } } } clash_entry ** PushClashPtr(memory_book *C, clash_entry *E) { clash_entry **New = MakeSpaceInBook(C); *New = E; return New; } #if DEBUG_CLASH_RESOLVER void PrintClash(memory_book *C, clash_entry *E) { PrintC(CS_CYAN, ResolutionStatusStrings[E->Status]); PrintC(E->Vacating ? CS_GREEN : CS_RED, " V"); PrintC(E->Superseding ? CS_GREEN : CS_RED, " S "); PrintLineage(E->Project->Lineage, FALSE); Print(stderr, "/"); PrintStringC(CS_MAGENTA, Wrap0i(E->CandidateBaseFilename)); Print(stderr, ": "); string CurrentOutputValue = Wrap0i(E->CurrentOutputValue); string DesiredOutputValue = Wrap0i(E->DesiredOutputValue); if(CurrentOutputValue.Length > 0) { PrintStringC(E->Vacating ? CS_BLACK_BOLD : CS_WHITE, CurrentOutputValue); } else { PrintStringC(CS_YELLOW, Wrap0("(null)")); } if( #if 0 !E->Vacating && StringsDiffer(CurrentOutputValue, DesiredOutputValue) #else 1 #endif ) { Print(stderr, " → "); string IncumbentBaseFilename = Wrap0i(E->IncumbentBaseFilename); clash_entry *Incumbent = GetClash(C, E->Project, IncumbentBaseFilename); if(Incumbent #if 0 && !Incumbent->Vacating #else || IncumbentBaseFilename.Length > 0 #endif ) { PrintStringC(CS_RED_BOLD, DesiredOutputValue); Print(stderr, " ("); PrintStringC(CS_MAGENTA, IncumbentBaseFilename); Print(stderr, ")"); } else { PrintStringC(CS_GREEN_BOLD, DesiredOutputValue); } } } void PrintClashes(memory_book *C, int LineNumber) { if(C->ItemCount) { Print(stderr, "\n"); PrintLinedFunctionName(LineNumber, "PrintClashes()"); } for(int i = 0; i < C->ItemCount; ++i) { clash_entry *This = GetPlaceInBook(C, i); PrintClash(C, This); Print(stderr, "\n"); } } void PrintClashChain(clash_resolver *R) { if(R->Chain.ItemCount) { } for(int i = 0; i < R->Chain.ItemCount; ++i) { clash_entry **This = GetPlaceInBook(&R->Chain, i); Colourise(CS_BLACK_BOLD); Print(stderr, " [%i] ", i); Colourise(CS_END); PrintClash(R->Main, *This); Print(stderr, "\n"); } } #endif // // NOTE(matt): Clash Detection typedef enum { PAGE_PLAYER = 1 << 0, PAGE_SEARCH = 1 << 1 } page_type; // NOTE(matt): Perhaps this OffsetLandmarks() could be made redundant once we're on the LUT void OffsetLandmarks(buffer *Dest, buffer *Src, page_type PageType) { if(Config->QueryString.Length > 0) { for(int i = 0; i < Assets.ItemCount; ++i) { asset *Asset = GetPlaceInBook(&Assets, i); if(PageType == PAGE_PLAYER) { for(int LandmarkIndex = 0; LandmarkIndex < Asset->Player.ItemCount; ++LandmarkIndex) { landmark *This = GetPlaceInBook(&Asset->Player, LandmarkIndex); if(This->BufferID == Src->ID) { This->Offset += Dest->Ptr - Dest->Location; This->BufferID = Dest->ID; } } } else { for(int LandmarkIndex = 0; LandmarkIndex < Asset->Search.ItemCount; ++LandmarkIndex) { landmark *This = GetPlaceInBook(&Asset->Search, LandmarkIndex); if(This->BufferID == Src->ID) { This->Offset += Dest->Ptr - Dest->Location; This->BufferID = Dest->ID; } } } } } } #define CopyLandmarkedBuffer(Dest, Src, PrevStartLinkOffset, PageType) CopyLandmarkedBuffer_(__LINE__, Dest, Src, PrevStartLinkOffset, PageType) void CopyLandmarkedBuffer_(int LineNumber, buffer *Dest, buffer *Src, uint32_t *PrevStartLinkOffset, page_type PageType) { if((Dest->Ptr - Dest->Location + Src->Ptr - Src->Location) >= Dest->Size) { Print(stderr, "CopyLandmarkedBuffer(%s) call on line %d cannot accommodate %ld(+1)-character %s\n", BufferIDStrings[Dest->ID], LineNumber, StringLength(Src->Location), BufferIDStrings[Src->ID]); __asm__("int3"); } if(PrevStartLinkOffset) { *PrevStartLinkOffset += Dest->Ptr - Dest->Location; } OffsetLandmarks(Dest, Src, PageType); Dest->Ptr += CopyBytes(Dest->Ptr, Src->Location, Src->Ptr - Src->Location); *Dest->Ptr = '\0'; } #define CopyBufferSized(Dest, Src, Size) CopyBufferSized_(__LINE__, Dest, Src, Size) void CopyBufferSized_(int LineNumber, buffer *Dest, buffer *Src, uint64_t Size) { // NOTE(matt): Similar to CopyBuffer(), just without null-terminating if((Dest->Ptr - Dest->Location + Size) > Dest->Size) { Print(stderr, "CopyBufferSized(%s) call on line %d cannot accommodate %ld-character %s\n", BufferIDStrings[Dest->ID], LineNumber, Size, BufferIDStrings[Src->ID]); __asm__("int3"); } Dest->Ptr += CopyBytes(Dest->Ptr, Src->Location, Size); } void AppendBuffer(buffer *Dest, buffer *Src) { uint64_t BytePosition = Dest->Ptr - Dest->Location; Dest->Size = Dest->Ptr - Dest->Location + Src->Ptr - Src->Location + 1; Dest->Location = realloc(Dest->Location, Dest->Size); Dest->Ptr = Dest->Location + BytePosition; CopyBuffer(Dest, Src); } void AppendLandmarkedBuffer(buffer *Dest, buffer *Src, page_type PageType) { OffsetLandmarks(Dest, Src, PageType); AppendBuffer(Dest, Src); } void AppendStringToBuffer(buffer *B, string S) { uint64_t BytePosition = B->Ptr - B->Location; B->Size = B->Ptr - B->Location + S.Length + 1; B->Location = realloc(B->Location, B->Size); B->Ptr = B->Location + BytePosition; CopyStringToBufferNoFormat(B, S); } void AppendInt32ToBuffer(buffer *B, int32_t I) { int Digits = DigitsInInt(&I); uint64_t BytePosition = B->Ptr - B->Location; B->Size = B->Ptr - B->Location + Digits + 1; B->Location = realloc(B->Location, B->Size); B->Ptr = B->Location + BytePosition; char Temp[Digits + 1]; sprintf(Temp, "%i", I); CopyStringToBufferNoFormat(B, Wrap0(Temp)); } void AppendUint32ToBuffer(buffer *B, uint32_t I) { int Digits = DigitsInUint(&I); uint64_t BytePosition = B->Ptr - B->Location; B->Size = B->Ptr - B->Location + Digits + 1; B->Location = realloc(B->Location, B->Size); B->Ptr = B->Location + BytePosition; char Temp[Digits + 1]; sprintf(Temp, "%i", I); CopyStringToBufferNoFormat(B, Wrap0(Temp)); } void AppendInt64ToBuffer(buffer *B, int64_t I) { int Digits = DigitsInInt(&I); uint64_t BytePosition = B->Ptr - B->Location; B->Size = B->Ptr - B->Location + Digits + 1; B->Location = realloc(B->Location, B->Size); B->Ptr = B->Location + BytePosition; char Temp[Digits + 1]; sprintf(Temp, "%li", I); CopyStringToBufferNoFormat(B, Wrap0(Temp)); } void AppendUint64ToBuffer(buffer *B, uint64_t I) { int Digits = DigitsInUint(&I); uint64_t BytePosition = B->Ptr - B->Location; B->Size = B->Ptr - B->Location + Digits + 1; B->Location = realloc(B->Location, B->Size); B->Ptr = B->Location + BytePosition; char Temp[Digits + 1]; sprintf(Temp, "%li", I); CopyStringToBufferNoFormat(B, Wrap0(Temp)); } uint64_t AmpersandEncodedStringLength(string S) { uint64_t Result = S.Length; for(int i = 0; i < S.Length; ++i) { switch(S.Base[i]) { case '<': case '>': Result += 3; break; case '&': case '\'': Result += 4; break; case '\"': Result += 5; break; default: break; } } return Result; } void AppendStringToBufferHTMLSafe(buffer *B, string S) { uint64_t BytePosition = B->Ptr - B->Location; B->Size = B->Ptr - B->Location + AmpersandEncodedStringLength(S) + 1; B->Location = realloc(B->Location, B->Size); B->Ptr = B->Location + BytePosition; CopyStringToBufferHTMLSafe(B, S); } void IndentBuffer(buffer *B, uint64_t IndentationLevel) { for(int i = 0; i < INDENT_WIDTH * IndentationLevel; ++i) { AppendStringToBuffer(B, Wrap0(" ")); } } 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; } void ResolvePath(char **Path) { buffer B; ClaimBuffer(&B, BID_RESOLVED_PATH, StringLength(*Path) + 1); CopyStringToBufferNoFormat(&B, Wrap0(*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, Wrap0(NextComponentHead)); Clear(B.Ptr, B.Size - (B.Ptr - B.Location)); B.Ptr -= RemainingChars; } Free(*Path); ExtendString0(Path, Wrap0(B.Location)); DeclaimBuffer(&B); } char * ExpandPath(string Path, string *RelativeToFile) { int Flags = WRDE_NOCMD | WRDE_UNDEF | WRDE_APPEND; wordexp_t Expansions = {}; char *WorkingPath = MakeString0("l", &Path); int wordexpResult = wordexp(WorkingPath, &Expansions, Flags); if(wordexpResult) { switch(wordexpResult) { case WRDE_BADCHAR: { SystemError(0, 0, S_ERROR, "wordexp: Illegal occurrence of newline or one of |, &, ;, <, >, (, ), {, } in: ", &Path); } break; case WRDE_BADVAL: { SystemError(0, 0, S_ERROR, "wordexp: An undefined shell variable was referenced, and the WRDE_UNDEF flag told us to consider this an error, in: ", &Path); } break; case WRDE_CMDSUB: { SystemError(0, 0, S_ERROR, "wordexp: Command substitution requested, but the WRDE_NOCMD flag told us to consider this an error, in: ", &Path); } break; case WRDE_NOSPACE: { SystemError(0, 0, S_ERROR, "wordexp: Out of memory", 0); } break; case WRDE_SYNTAX: { SystemError(0, 0, S_ERROR, "wordexp: Shell syntax error, such as unbalanced parentheses or unmatched quotes, in: ", &Path); } break; } } Free(WorkingPath); char *Result = 0; if(Expansions.we_wordc > 0) { if(Expansions.we_wordv[0][0] != '/') { if(RelativeToFile) { string RelativeDir = StripComponentFromPath(*RelativeToFile); Result = MakeString0("lss", &RelativeDir, "/", Expansions.we_wordv[0]); } else { Result = MakeString0("sss", getenv("PWD"), "/", Expansions.we_wordv[0]); } } else { Result = MakeString0("s", Expansions.we_wordv[0]); } } wordfree(&Expansions); if(Result) { ResolvePath(&Result); } return Result; } bool MakeDir(string Path) { for(int i = 1; i < Path.Length; ++i) { if(Path.Base[i] == '/') { Path.Base[i] = '\0'; int ReturnCode = mkdir(Path.Base, 00755); Path.Base[i] = '/'; if(ReturnCode == -1 && errno == EACCES) { return FALSE; } } } char *FullPath = MakeString0("l", &Path); int ReturnCode = mkdir(FullPath, 00755); Free(FullPath); if(ReturnCode == -1 && errno == EACCES) { return FALSE; } return TRUE; } char * GetDirectoryPath(char *Filepath) { char *Ptr = Filepath + StringLength(Filepath) - 1; while(Ptr > Filepath && *Ptr != '/') { --Ptr; } if(Ptr == Filepath) { *Ptr++ = '.'; } *Ptr = '\0'; return Filepath; } void PushTemplateTag(template *Template, int Offset, template_tag_code TagCode, asset *Asset, navigation_buffer *NavBuffer) { tag_offset *This = MakeSpaceInBook(&Template->Metadata.Tags); This->Offset = Offset; This->TagCode = TagCode; This->Asset = Asset; This->Nav = NavBuffer; } void InitTemplate(template *Template, string Location, template_type Type) { Template->Metadata.Type = Type; if(Type == TEMPLATE_GLOBAL_SEARCH) { Template->File = InitFile(&Config->GlobalTemplatesDir, &Config->GlobalSearchTemplatePath, EXT_NULL, FALSE); } else { if(Location.Base[0] == '/') { Template->File = InitFile(0, &Location, EXT_NULL, FALSE); } else { Template->File = InitFile(Type == TEMPLATE_BESPOKE ? &CurrentProject->HMMLDir : &CurrentProject->TemplatesDir, &Location, EXT_NULL, FALSE); } } Print(stderr, "%sPacking%s template: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], Template->File.Path); ReadFileIntoBuffer(&Template->File); ClearTemplateMetadata(Template); Template->Metadata.Tags = InitBook(sizeof(tag_offset), 16); Template->Metadata.NavBuffer = InitBook(sizeof(navigation_buffer), 4); } v4 V4(int A, int B, int C, int D) { v4 Result = { .A = A, .B = B, .C = C, .D = D }; return Result; } float TimecodeToDottedSeconds(v4 Timecode) { return (float)Timecode.Hours * SECONDS_PER_HOUR + (float)Timecode.Minutes * SECONDS_PER_MINUTE + (float)Timecode.Seconds + (float)Timecode.Milliseconds / 1000; } 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; } // TODO(matt): Use ExtendString0() // AFD void ConstructURLPrefix(buffer *URLPrefix, asset_type AssetType, enum8(pages) PageType) { RewindBuffer(URLPrefix); if(Config->AssetsRootURL.Length > 0) { CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config->AssetsRootURL); CopyStringToBuffer(URLPrefix, "/"); } else { if(PageType == PAGE_PLAYER) { CopyStringToBuffer(URLPrefix, "../"); } CopyStringToBuffer(URLPrefix, "../"); } switch(AssetType) { case ASSET_CSS: if(Config->CSSDir.Length > 0) { CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config->CSSDir); CopyStringToBuffer(URLPrefix, "/"); } break; case ASSET_IMG: if(Config->ImagesDir.Length > 0 ) { CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config->ImagesDir); CopyStringToBuffer(URLPrefix, "/"); } break; case ASSET_JS: if(Config->JSDir.Length > 0) { CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config->JSDir); CopyStringToBuffer(URLPrefix, "/"); } break; default: break; } } typedef struct { hsl_colour Colour; person *Person; bool Seen; } speaker; typedef struct { _memory_book(speaker) Speakers; abbreviation_scheme AbbrevScheme; } speakers; enum { CreditsError_NoCredentials, CreditsError_NoHost, CreditsError_NoIndexer, CreditsError_NoRole, } 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); } char * ConstructAssetPath(file *AssetFile, string Filename, asset_type Type) { char *Result = 0; if(Config->AssetsRootDir.Length > 0) { ExtendString0(&Result, Config->AssetsRootDir); } string AssetDir = {}; switch(Type) { case ASSET_CSS: AssetDir = Config->CSSDir; break; case ASSET_IMG: AssetDir = Config->ImagesDir; break; case ASSET_JS: AssetDir = Config->JSDir; break; default: break; } if(AssetDir.Length > 0) { ExtendString0(&Result, Wrap0("/")); ExtendString0(&Result, AssetDir); } if(Filename.Length > 0) { ExtendString0(&Result, Wrap0("/")); ExtendString0(&Result, Filename); } return Result; } void CycleFile(file *File) { CloseFile(File, FALSE); // 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); } void SetSignpostOffset(buffer *B, file_edit *E, file_signpost *S) { S->Byte = (char *)S->Ptr - B->Location; if(S->Byte >= E->BytePosition) { S->Byte += E->Size; } } void CycleSignpostedFile(file_signposted *File) { // NOTE(matt) This file_signposted struct is totally hardcoded to the Metadata file, but may suffice for now // // TODO(matt): This is probably insufficient. We likely need to offset the pointers any time we add stuff to the database CloseFile(&File->File, FALSE); // 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 file_signposts *S = &File->Signposts; buffer *B = &File->File.Buffer; uint64_t PtrByte = B->Ptr - B->Location; if(S->ProjectsBlock.Ptr) { SetSignpostOffset(B, &S->Edit, &S->ProjectsBlock); } if(S->ProjectParent.Ptr) { SetSignpostOffset(B, &S->Edit, &S->ProjectParent); } if(S->ProjectHeader.Ptr) { SetSignpostOffset(B, &S->Edit, &S->ProjectHeader); } if(S->Prev.Ptr) { SetSignpostOffset(B, &S->Edit, &S->Prev); } if(S->This.Ptr) { SetSignpostOffset(B, &S->Edit, &S->This); } if(S->Next.Ptr) { SetSignpostOffset(B, &S->Edit, &S->Next); } if(S->AssetsBlock.Ptr) { SetSignpostOffset(B, &S->Edit, &S->AssetsBlock); } FreeBuffer(B); ReadFileIntoBuffer(&File->File); B->Ptr = B->Location + PtrByte; if(S->ProjectsBlock.Ptr) { S->ProjectsBlock.Ptr = B->Location + S->ProjectsBlock.Byte; } if(S->ProjectParent.Ptr) { S->ProjectParent.Ptr = B->Location + S->ProjectParent.Byte; } if(S->ProjectHeader.Ptr) { S->ProjectHeader.Ptr = B->Location + S->ProjectHeader.Byte; } if(S->Prev.Ptr) { S->Prev.Ptr = B->Location + S->Prev.Byte; } if(S->This.Ptr) { S->This.Ptr = B->Location + S->This.Byte; } if(S->Next.Ptr) { S->Next.Ptr = B->Location + S->Next.Byte; } if(S->AssetsBlock.Ptr) { S->AssetsBlock.Ptr = B->Location + S->AssetsBlock.Byte; } S->Edit.BytePosition = 0; S->Edit.Size = 0; } void ClearTerminalRow(uint64_t Length) { for(uint64_t i = 0; i < Length; ++i) { Print(stderr, "\b \b"); } } typedef struct { uint32_t First; uint32_t Length; } landmark_range; bool ProjectIndicesMatch(db_project_index A, db_project_index B) { return (A.Generation == B.Generation && A.Index == B.Index); } db_landmark * LocateFirstLandmark(db_asset *A) { char *Ptr = (char *)A; Ptr += sizeof(*A); return (db_landmark *)Ptr; } db_landmark * LocateLandmark(db_asset *A, uint64_t Index) { char *Ptr = (char *)A; Ptr += sizeof(*A) + sizeof(db_landmark) * Index; return (db_landmark *)Ptr; } uint32_t GetIndexRangeLength(db_asset *A, landmark_range ProjectRange, uint64_t LandmarkIndex) { uint32_t Result = 0; db_landmark *FirstLandmarkOfRange = LocateLandmark(A, LandmarkIndex); db_landmark *Landmark = FirstLandmarkOfRange; while(LandmarkIndex < (ProjectRange.First + ProjectRange.Length) && ProjectIndicesMatch(FirstLandmarkOfRange->Index.Project, Landmark->Index.Project) && FirstLandmarkOfRange->Index.Entry == Landmark->Index.Entry) { ++Result; ++LandmarkIndex; ++Landmark; } return Result; } landmark_range GetIndexRange(db_asset *A, landmark_range ProjectRange, uint64_t LandmarkIndex) { landmark_range Result = {}; Result.First = LandmarkIndex; db_landmark *LandmarkInRange = LocateLandmark(A, LandmarkIndex); db_landmark *Landmark = LandmarkInRange; while(Result.First > ProjectRange.First && ProjectIndicesMatch(LandmarkInRange->Index.Project, Landmark->Index.Project) && LandmarkInRange->Index.Entry == Landmark->Index.Entry) { --Result.First; --Landmark; } if(LandmarkInRange->Index.Entry != Landmark->Index.Entry) { ++Result.First; } Result.Length = GetIndexRangeLength(A, ProjectRange, Result.First); return Result; } int ProjectIndicesDiffer(db_project_index A, db_project_index B) { if(A.Generation < B.Generation || (A.Generation == B.Generation && A.Index < B.Index)) { return -1; } else if(A.Generation > B.Generation || (A.Generation == B.Generation && A.Index > B.Index)) { return 1; } else { return 0; } } landmark_range BinarySearchForMetadataLandmark(db_asset *Asset, landmark_range ProjectRange, int EntryIndex) { landmark_range Result = ProjectRange; if(ProjectRange.Length > 0) { db_landmark *FirstLandmark = LocateFirstLandmark(Asset); uint64_t Lower = ProjectRange.First; db_landmark *LowerLandmark = FirstLandmark + Lower; uint64_t Upper = Lower + ProjectRange.Length - 1; db_landmark *UpperLandmark = FirstLandmark + Upper; if(EntryIndex < LowerLandmark->Index.Entry) { Result.First = Lower; Result.Length = 0; return Result; } // TODO(matt): Is there a slicker way of doing this? if(EntryIndex > UpperLandmark->Index.Entry) { Result.First = Upper + 1; Result.Length = 0; return Result; } int Pivot = Upper - ((Upper - Lower) >> 1); db_landmark *PivotLandmark; do { LowerLandmark = FirstLandmark + Lower; PivotLandmark = FirstLandmark + Pivot; UpperLandmark = FirstLandmark + Upper; if(EntryIndex == LowerLandmark->Index.Entry) { return GetIndexRange(Asset, ProjectRange, Lower); } if(EntryIndex == PivotLandmark->Index.Entry) { return GetIndexRange(Asset, ProjectRange, Pivot); } if(EntryIndex == UpperLandmark->Index.Entry) { return GetIndexRange(Asset, ProjectRange, Upper); } if(EntryIndex < PivotLandmark->Index.Entry) { Upper = Pivot; } else { Lower = Pivot; } Pivot = Upper - ((Upper - Lower) >> 1); } while(Upper > Pivot); Result.First = Upper; Result.Length = 0; return Result; } return Result; } void PrintEntryIndex(db_project_index Project, int64_t EntryIndex) { Print(stderr, "%s%2i:%2i%s %s%3li%s", ColourStrings[CS_MAGENTA], Project.Generation, Project.Index, ColourStrings[CS_END], ColourStrings[CS_BLUE_BOLD], EntryIndex, ColourStrings[CS_END]); } void PrintLandmark(db_landmark *L, uint16_t *Index) { if(Index) { Colourise(CS_BLACK_BOLD); Print(stderr, "[%4u] ", *Index); Colourise(CS_END); } PrintEntryIndex(L->Index.Project, L->Index.Entry); Print(stderr, " %6u", L->Position); } void PrintAsset(db_asset *A, uint16_t *Index) { if(Index) { Colourise(CS_BLACK_BOLD); Print(stderr, "[%4u]", *Index); Colourise(CS_END); } string FilenameL = Wrap0i(A->Filename); Print(stderr, " %s asset: %.*s [%8x]\n", AssetTypeNames[A->Type], (int)FilenameL.Length, FilenameL.Base, A->Hash); if(A->Type == ASSET_IMG) { bool Associated = A->Associated; uint64_t Variants = A->Variants; if(Index) { int SpacesRequired = 1 + 1 + DigitsInInt(Index); for(int i = 0; i < SpacesRequired; ++i) { Print(stderr, " "); } } Print(stderr, " Associated: "); if(Associated) { PrintC(CS_GREEN, "true"); } else { PrintC(CS_RED, "false"); } Print(stderr, " • Variants: "); Colourise(CS_BLUE_BOLD); Print(stderr, "%lu", Variants); Colourise(CS_END); Print(stderr, " • Dimensions: "); Colourise(CS_BLUE_BOLD); Print(stderr, "%u", A->Width); Print(stderr, "×"); Print(stderr, "%u", A->Height); Colourise(CS_END); Print(stderr, "\n"); } } void PrintAssetAndLandmarks(db_asset *A, uint16_t *Index) { Print(stderr, "\n" "\n"); PrintAsset(A, Index); db_landmark *FirstLandmark = LocateFirstLandmark(A); for(uint16_t i = 0; i < A->LandmarkCount; ++i) { db_landmark *This = FirstLandmark + i; if((i % 8) == 0) { Print(stderr, "\n"); } else { PrintC(CS_BLACK_BOLD, " │ "); } PrintLandmark(This, &i); } Print(stderr, "\n"); } // TODO(matt): Almost definitely redo this using Locate*() functions... void SnipeChecksumAndCloseFile(file *HTMLFile, db_asset *Asset, int LandmarksInFile, asset_hash Checksum, int64_t *RunningLandmarkIndex) { db_landmark *FirstLandmark = LocateFirstLandmark(Asset); for(int j = 0; j < LandmarksInFile; ++j, ++*RunningLandmarkIndex) { db_landmark *Landmark = FirstLandmark + *RunningLandmarkIndex; HTMLFile->Buffer.Ptr = HTMLFile->Buffer.Location + Landmark->Position; char ChecksumString[16]; ClearCopyString(ChecksumString, sizeof(ChecksumString), "%08x", Checksum); CopyStringToBufferNoTerminate(&HTMLFile->Buffer, Wrap0(ChecksumString)); } HTMLFile->Handle = fopen(HTMLFile->Path, "w"); fwrite(HTMLFile->Buffer.Location, HTMLFile->Buffer.Size, 1, HTMLFile->Handle); FreeFile(HTMLFile, NA); } void * SkipHeaderedSection(void *LocationInDB, db_structure_headered_section *Structure) { char *Ptr = LocationInDB; uint64_t Count = ReadDBField(LocationInDB, Structure->EntryCount); Ptr += Structure->Self.SizeOf + Structure->Entry.SizeOf * Count; return Ptr; } void * SkipHeaderedSectionRecursively(void *LocationInDB, db_structure_headered_section *Structure) { uint64_t ChildCount = ReadDBField(LocationInDB, Structure->ChildCount); char *Ptr = SkipHeaderedSection(LocationInDB, Structure); for(int i = 0; i < ChildCount; ++i) { Ptr = SkipHeaderedSectionRecursively(Ptr, Structure); } return Ptr; } typedef struct { uint64_t CurrentGeneration; _memory_book(uint32_t) EntriesInGeneration; } project_generations; db_project_index GetCurrentProjectIndex(project_generations *G) { db_project_index Result = {}; Result.Generation = G->CurrentGeneration; Result.Index = *(uint32_t *)GetPlaceInBook(&G->EntriesInGeneration, G->CurrentGeneration); return Result; } void AddEntryToGeneration(project_generations *G, project *P) { if(G) { if(G->EntriesInGeneration.ItemCount <= G->CurrentGeneration) { MakeSpaceInBook(&G->EntriesInGeneration); } if(P) { P->Index = GetCurrentProjectIndex(G); } uint32_t *This = GetPlaceInBook(&G->EntriesInGeneration, G->CurrentGeneration); ++*This; } } void IncrementCurrentGeneration(project_generations *G) { if(G) { ++G->CurrentGeneration; } } void DecrementCurrentGeneration(project_generations *G) { if(G) { --G->CurrentGeneration; } } void PrintGenerations(project_generations *G, bool IndicateCurrentGeneration) { for(uint64_t i = 0; i < G->EntriesInGeneration.ItemCount; ++i) { uint32_t *This = GetPlaceInBook(&G->EntriesInGeneration, i); Print(stderr, "%lu: %u%s\n", i, *This, IndicateCurrentGeneration && i == G->CurrentGeneration ? " [Current Generation]" : ""); } } void * SkipBlock(void *Block) { void *Result = 0; uint32_t FirstInt = *(uint32_t *)Block; db_structure_block *BlockStructure = GetDBStructureBlock(FirstInt); Assert(BlockStructure); Result = (char *)Block; Result += BlockStructure->SizeOf; uint64_t Count = ReadDBField(Block, BlockStructure->Count); if(BlockStructure->Entry.SizeOf) { Result += BlockStructure->Entry.SizeOf * Count; } else { for(int i = 0; i < Count; ++i) { Result = SkipHeaderedSectionRecursively(Result, &BlockStructure->HeaderedSection); } } return Result; } void * LocateBlock(block_id BlockID) { void *Result = 0; db_header *Header = (db_header *)DB.Metadata.File.Buffer.Location; char *Ptr = (char *)Header; Ptr += sizeof(db_header); for(int i = 0; i < Header->BlockCount; ++i) { uint32_t FirstInt = *(uint32_t *)Ptr; if(FirstInt == GetFourFromBlockID(BlockID)) { Result = Ptr; break; } else { Ptr = SkipBlock(Ptr); } } return Result; } db_header_project * LocateFirstChildProjectOfBlock(db_block_projects *P) { char *Ptr = (char *)P; Ptr += sizeof(*P); return (db_header_project *)Ptr; } db_header_project * LocateFirstChildProject(db_header_project *P) { char *Ptr = (char *)P; Ptr += sizeof(*P) + sizeof(db_entry) * P->EntryCount; return (db_header_project *)Ptr; } // TODO(matt): Test this project index accumulation stuff, for goodness' sake! db_header_project * LocateProjectRecursively(db_header_project *Header, db_project_index *DesiredProject, db_project_index *Accumulator) { db_header_project *Result = 0; db_header_project *Child = LocateFirstChildProject(Header); for(int i = 0; i < Header->ChildCount; ++i) { if(Accumulator->Generation == DesiredProject->Generation) { if(Accumulator->Index == DesiredProject->Index) { Result = Child; break; } else { ++Accumulator->Index; } } ++Accumulator->Generation; db_header_project *Test = LocateProjectRecursively(Child, DesiredProject, Accumulator); if(Test) { Result = Test; break; } --Accumulator->Generation; Child = SkipHeaderedSectionRecursively(Child, GetDBStructureHeaderedSection(B_PROJ)); } return Result; } db_structure * GetDBStructure(uint32_t Version) { db_structure *Result = 0; for(int i = 0; i < DBStructures.Structure.ItemCount; ++i) { db_structure *This = GetPlaceInBook(&DBStructures.Structure, i); if(This->Version == Version) { Result = This; break; } } return Result; } db_header_project * LocateProject(db_project_index Project) { db_header_project *Result = 0; if(Project.Generation != -1) { db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr ? DB.Metadata.Signposts.ProjectsBlock.Ptr : LocateBlock(B_PROJ); db_header_project *Child = LocateFirstChildProjectOfBlock(ProjectsBlock); db_project_index Accumulator = {}; if(ProjectsBlock) { db_structure_headered_section *DBStructureProjects = GetDBStructureHeaderedSection(B_PROJ); if(Project.Generation == Accumulator.Generation) { for(int i = 0; i < ProjectsBlock->Count; ++i) { if(Project.Index == Accumulator.Index) { Result = Child; break; } Child = SkipHeaderedSectionRecursively(Child, DBStructureProjects); ++Accumulator.Index; } } else { ++Accumulator.Generation; for(int i = 0; i < ProjectsBlock->Count; ++i) { db_header_project *Test = LocateProjectRecursively(Child, &Project, &Accumulator); if(Test) { Result = Test; break; } Child = SkipHeaderedSectionRecursively(Child, DBStructureProjects); } --Accumulator.Generation; } } } return Result; } db_entry * LocateFirstEntry(db_header_project *P) { char *Ptr = (char *)P; Ptr += sizeof(db_header_project); return (db_entry *)Ptr; } db_entry * LocateEntryOfProject(db_header_project *Project, int32_t EntryIndex) { db_entry *Result = 0; if(Project && EntryIndex < Project->EntryCount) { char *Ptr = (char *)Project; Ptr += sizeof(db_header_project) + sizeof(db_entry) * EntryIndex; Result = (db_entry *)Ptr; } return Result; } db_entry * LocateEntry(db_project_index DBProjectIndex, int32_t EntryIndex) { return LocateEntryOfProject(LocateProject(DBProjectIndex), EntryIndex); } void SetFileEditPosition(file_signposted *File) { File->Signposts.Edit.BytePosition = ftell(File->File.Handle); File->Signposts.Edit.Size = 0; } void AccumulateFileEditSize(file_signposted *File, int64_t Bytes) { File->Signposts.Edit.Size += Bytes; } FILE * OpenFileForWriting(file *F) { if(F->Locking && F->Handle) { fclose(F->Handle); } F->Handle = fopen(F->Path, "w"); TryLock(F); return F->Handle; } // TODO(matt): Consider enforcing an order of these blocks, basically putting the easy-to-skip ones first... void * InitBlock(block_id ID) { db_header *Header = (db_header *)DB.Metadata.File.Buffer.Location; ++Header->BlockCount; OpenFileForWriting(&DB.Metadata.File); fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle); SetFileEditPosition(&DB.Metadata); switch(ID) { case B_ASET: { db_block_assets Block = {}; fwrite(&Block, sizeof(Block), 1, DB.Metadata.File.Handle); AccumulateFileEditSize(&DB.Metadata, sizeof(Block)); break; } case B_PROJ: { db_block_projects Block = {}; fwrite(&Block, sizeof(Block), 1, DB.Metadata.File.Handle); AccumulateFileEditSize(&DB.Metadata, sizeof(Block)); break; } } uint32_t BytesIntoFile = DB.Metadata.File.Buffer.Size; CycleSignpostedFile(&DB.Metadata); return &DB.Metadata.File.Buffer.Location + BytesIntoFile; } db_asset * LocateFirstAsset(db_block_assets *B) { char *Ptr = (char *)B; Ptr += sizeof(*B); return (db_asset *)Ptr; } #define PrintAssetsBlock(B) PrintAssetsBlock_(B, __LINE__) void * PrintAssetsBlock_(db_block_assets *B, int LineNumber) { #if 0 PrintLinedFunctionName(LineNumber, "PrintAssetsBlock()"); #endif if(!B) { B = LocateBlock(B_ASET); } PrintC(CS_BLUE_BOLD, "\n" "\n" "Assets Block (ASET)"); db_asset *A = LocateFirstAsset(B); for(uint16_t i = 0; i < B->Count; ++i) { PrintAssetAndLandmarks(A, &i); A = SkipHeaderedSectionRecursively(A, GetDBStructureHeaderedSection(B_ASET)); } return A; } char * ConstructDirectoryPath(string *BaseDir, string *PageLocation, string *EntryOutput) { char *Result = 0; if(BaseDir) { ExtendString0(&Result, *BaseDir); } if(PageLocation && PageLocation->Length > 0) { if(BaseDir) { ExtendString0(&Result, Wrap0("/")); } ExtendString0(&Result, *PageLocation); } if(EntryOutput) { ExtendString0(&Result, Wrap0("/")); ExtendString0(&Result, *EntryOutput); } return Result; } char * ConstructHTMLIndexFilePath(string *BaseDir, string *PageLocation, string *EntryOutput) { char *Result = ConstructDirectoryPath(BaseDir, PageLocation, EntryOutput); ExtendString0(&Result, Wrap0("/index.html")); return Result; } rc ReadSearchPageIntoBuffer(file *File, string *BaseDir, string *SearchLocation) { File->Path = ConstructHTMLIndexFilePath(BaseDir, SearchLocation, 0); return ReadFileIntoBuffer(File); } string GetGlobalSearchPageLocation(void) { string Result = {}; db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr ? DB.Metadata.Signposts.ProjectsBlock.Ptr : LocateBlock(B_PROJ); Result = Wrap0i(ProjectsBlock->GlobalSearchDir); return Result; } rc ReadGlobalSearchPageIntoBuffer(file *File) { string SearchLocationL = GetGlobalSearchPageLocation(); File->Path = ConstructHTMLIndexFilePath(0, &SearchLocationL, 0); return ReadFileIntoBuffer(File); } rc ReadPlayerPageIntoBuffer(file *File, string BaseDir, string PlayerLocation, string OutputLocation) { File->Path = ConstructHTMLIndexFilePath(&BaseDir, &PlayerLocation, &OutputLocation); return ReadFileIntoBuffer(File); } rc SnipeChecksumIntoHTML(db_asset *Asset, asset_hash Checksum) { db_landmark *FirstLandmark = LocateFirstLandmark(Asset); rc Result = RC_SUCCESS; for(int64_t RunningLandmarkIndex = 0; RunningLandmarkIndex < Asset->LandmarkCount;) { db_landmark *Landmark = FirstLandmark + RunningLandmarkIndex; // TODO(matt): Do the Next vs Current Project check to see whether we even need to do LocateProject() db_header_project *P = LocateProject(Landmark->Index.Project); file HTML = {}; rc FileReadRC = RC_INIT; if(Landmark->Index.Entry >= 0) { db_entry *Entry = LocateEntry(Landmark->Index.Project, Landmark->Index.Entry); if(Entry) { string BaseDir = Wrap0i(P->BaseDir); string PlayerLocation = Wrap0i(P->PlayerLocation); string OutputLocation = Wrap0i(Entry->OutputLocation); FileReadRC = ReadPlayerPageIntoBuffer(&HTML, BaseDir, PlayerLocation, OutputLocation); } else { PrintC(CS_ERROR, "\nInvalid landmark (aborting update): "); PrintAsset(Asset, 0); PrintLandmark(Landmark, 0); Result = RC_FAILURE; break; } } else { switch(Landmark->Index.Entry) { case SP_SEARCH: { if(P) { string BaseDir = Wrap0i(P->BaseDir); string SearchLocation = Wrap0i(P->SearchLocation); FileReadRC = ReadSearchPageIntoBuffer(&HTML, &BaseDir, &SearchLocation); } else { FileReadRC = ReadGlobalSearchPageIntoBuffer(&HTML); } } break; default: { Colourise(CS_RED); Print(stderr, "SnipeChecksumIntoHTML() does not know about special page with index %i\n", Landmark->Index.Entry); Colourise(CS_END); Assert(0); } break; } } if(FileReadRC == RC_SUCCESS) { landmark_range Range = {}; Range.First = RunningLandmarkIndex; Range.Length = Asset->LandmarkCount - Range.First; int Length = GetIndexRangeLength(Asset, Range, RunningLandmarkIndex); SnipeChecksumAndCloseFile(&HTML, Asset, Length, Checksum, &RunningLandmarkIndex); } } return Result; } void PrintWatchFile(watch_file *W) { Print(stderr, "%s", WatchTypeStrings[W->Type]); Print(stderr, " • "); if(W->Extension != EXT_NULL) { Print(stderr, "*"); PrintString(ExtensionStrings[W->Extension]); } else { PrintString(W->Path); } if(W->Project) { Print(stderr, " • "); PrintLineage(W->Project->Lineage, FALSE); } if(W->Asset) { Print(stderr, " • "); Print(stderr, "%s", AssetTypeNames[W->Asset->Type]); } } void PrintWatchHandle(watch_handle *W) { Print(stderr, "%4d", W->Descriptor); Print(stderr, " • "); if(StringsDiffer(W->TargetPath, W->WatchedPath)) { PrintStringC(CS_MAGENTA, W->WatchedPath); Print(stderr, "\n "); PrintStringC(CS_RED_BOLD, W->TargetPath); } else { PrintStringC(CS_GREEN_BOLD, W->TargetPath); } for(int i = 0; i < W->Files.ItemCount; ++i) { watch_file *This = GetPlaceInBook(&W->Files, i); Print(stderr, "\n" " "); PrintWatchFile(This); } } #define PrintWatchHandles() PrintWatchHandles_(__LINE__) void PrintWatchHandles_(int LineNumber) { typography T = { .UpperLeftCorner = "┌", .UpperLeft = "╾", .Horizontal = "─", .UpperRight = "╼", .Vertical = "│", .LowerLeftCorner = "└", .LowerLeft = "╽", .Margin = " ", .Delimiter = ": ", .Separator = "•", }; Print(stderr, "\n" "%s%s%s [%i] PrintWatchHandles()", T.UpperLeftCorner, T.Horizontal, T.UpperRight, LineNumber); for(int i = 0; i < WatchHandles.Handles.ItemCount; ++i) { watch_handle *This = GetPlaceInBook(&WatchHandles.Handles, i); Print(stderr, "\n"); PrintWatchHandle(This); } Print(stderr, "\n" "%s%s%s", T.LowerLeftCorner, T.Horizontal, T.UpperRight); } bool IsSymlink(char *Filepath) { int File = open(Filepath, O_RDONLY | O_NOFOLLOW); bool Result = (errno == ELOOP); close(File); return Result; } watch_file * PushWatchFileUniquely(watch_handle *Handle, string Filepath, extension_id Extension, watch_type Type, project *Project, asset *Asset) { watch_file *Result = 0; bool Required = TRUE; for(int i = 0; i < Handle->Files.ItemCount; ++i) { watch_file *This = GetPlaceInBook(&Handle->Files, i); if(This->Type == Type) { if(Extension != EXT_NULL) { if(This->Extension == Extension) { Result = This; Required = FALSE; break; } } else if(StringsMatch(This->Path, Filepath)) { Result = This; EraseCurrentStringFromBook(&WatchHandles.Paths); Required = FALSE; break; } } } if(Required) { watch_file *New = MakeSpaceInBook(&Handle->Files); if(Extension != EXT_NULL) { New->Extension = Extension; } else { New->Path = Filepath; } New->Type = Type; New->Project = Project; New->Asset = Asset; Result = New; } return Result; } bool FileExists(string Path) { // TODO(matt): Whenever we need to produce a 0-terminated string like this, we may as well do: // WriteString0InBook() [...whatever stuff...] EraseCurrentStringFromBook() // NOTE(matt): Stack-string char Path0[Path.Length + 1]; CopyStringNoFormat(Path0, sizeof(Path0), Path); bool Result = TRUE; int File = open(Path0, O_RDONLY); if(File == -1) { Result = FALSE; } close(File); return Result; } typedef struct { string A; string B; } string_2x; string_2x WriteFilenameThenTargetPathIntoBook(memory_book *M, string Path, watch_type Type) // NOTE(matt): Follows symlinks { // NOTE(matt): The order in which we write the strings into the memory_book is deliberate and permits duplicate TargetPath // or Filename to be erased by PushWatchHandle() and PushWatchFileUniquely() string_2x Result = {}; // NOTE(matt): Stack-string char OriginalPath0[Path.Length + 1]; CopyStringNoFormat(OriginalPath0, sizeof(OriginalPath0), Path); // NOTE(matt): Stack-string char ResolvedSymlinkPath[4096] = {}; string FullPath = {}; if(IsSymlink(OriginalPath0)) { int PathLength = readlink(OriginalPath0, ResolvedSymlinkPath, 4096); FullPath = Wrap0i_(ResolvedSymlinkPath, PathLength); } else { FullPath = Path; } if(Type == WT_HMML) { Result.B = WriteStringInBook(M, FullPath); } else { Result.A = WriteStringInBook(M, GetFinalComponent(FullPath)); Result.B = WriteStringInBook(M, StripComponentFromPath(FullPath)); } return Result; } string GetNearestExistingPath(string Path) { string Result = Path; while(!FileExists(Result)) { Result = StripComponentFromPath(Result); } return Result; } watch_file * PushWatchHandle(string Path, extension_id Extension, watch_type Type, project *Project, asset *Asset) { watch_file *Result = 0; // NOTE(matt): This function influences RemoveAndFreeAllButFirstWatchFile(). If we change to write different strings into // the W->Paths memory_book, we must reflect that change over to RemoveAndFreeAllButFirstWatchFile() // NOTE(matt): Path types: // WT_ASSET: File, but we seem to strip the file component from the path, leaving the parent directory // WT_CONFIG: Directory of -c config, then File of existing config files // WT_HMML: Directory // // So do we need to watch Files and Directories differently? // // We could surely detect the type of Path we've been passed, whether DIR or FILE // // Rationale: // WT_HMML: We must watch the directory, so that we can actually pick up new .hmml files // HMML filenames are supposed to end .hmml, so we may use that to make sure we pick out the // watch handle // And we point to the Project so that we may switch to the correct CurrentProject // // For HMMLDir that don't exist, they may come into existence after the initial Sync. // How can we handle this? Must we watch the nearest existing ancestor directory, and keep // progressively swapping that out until we're watching the HMMLDir itself? // // Do we augment watch_handle with "NearestAncestorPath", in addition to TargetPath? // So if the TargetPath doesn't exist, we instead watch the NearestAncestorPath. The all watch // handles have a valid WatchDescriptor (i.e. not -1). // // WT_CONFIG: We must watch the -c directory, to enable us to handle the absence of a config // But thereafter, we may (and probably should) watch the files directly, as we are doing // The reason to watch at all is to enable live-reloading // WT_ASSET: We have a finite set of assets, right? So shouldn't that mean that we could just watch the // files directly? // // We have BuiltinAssets, which we push onto the Assets whether or not the file exists, but we // only push their watch handle if the file does exist. Maybe any time we create an asset file, // e.g. cinera_topics.css, or we see an asset file referred to in a template, we try pushing on // its watch handle. // // Q: Why watch at all? // A: So that we can rehash the file and snipe its new checksum into the html files // So wouldn't it be worth pointing to the Asset directly, so that we have immediate access to // this data? // // Permitting multiple "types" at the same path? // // This could only possibly happen for directories, which excludes WT_ASSET. // WT_HMML // We determine it to be a WT_HMML if the filepath ends .hmml // WT_CONFIG // The only WT_CONFIG directory is the parent of the -c, whose path we always know // // // Side issue: Why bother storing the whole path, when the Event->name only contains the BaseFilename? // Is it so we can distinguish between paths when pushing / getting watch handles? // // I reckon we store the Target and the Watched paths, both of which must surely be the full thing // // Symlinks: What do we watch? The symlink, or the path to which the symlink points? // // Okay, I reckon what we do is the following: // // watch_handle // { // char *TargetDirectory // char *WatchedDirectory // int FileCount // char **Files // } // // The Target and Files remain the same always // The WatchedDirectory may change according to which directories exist // // Watch the target directory // For each watch_handle, list out the specific files that we're watching // // We only need to write anything into the book once // // WrittenPath: // /home/matt/tmp/file // Filename: file // TargetPath: /home/matt/tmp // // If we write the WrittenPath, then realise that the TargetPath is already in there, we'd inadvertently // erase the whole WrittenPath // // If we write the Filename then the TargetPath, then realise the TargetPath is already there, we'd erase // the TargetPath just fine. But then if we later realise we have the Filename too, we have no way to erase // that. While suggests that we should straight up make the memory_book Current be part of a linked list, // so we could erase multiple things if needed. // // But, write the Filename, then the TargetPath: // TargetPath doesn't already exist // Filename, couldn't already exist // // TargetPath does exist, so may be erased // Filename // does exist, so may be erase // doesn't exist, so must remain // // Seems like we really want to "resolve" the whole path somewhere temporary (on the stack), then: // string Filename = GetFinalComponent(Wrap0(Temp)); // string TargetPath = StripComponentFromPath(Wrap0(Temp)); // WriteStringInBook(Filename); // WriteStringInBook(TargetPath); string_2x FilenameAndDir = WriteFilenameThenTargetPathIntoBook(&WatchHandles.Paths, Path, Type); string Filename = FilenameAndDir.A; string TargetPath = FilenameAndDir.B; watch_handle *Watch = 0; for(int i = 0; i < WatchHandles.Handles.ItemCount; ++i) { watch_handle *This = GetPlaceInBook(&WatchHandles.Handles, i); if(StringsMatch(TargetPath, This->TargetPath)) { Watch = This; EraseCurrentStringFromBook(&WatchHandles.Paths); break; } } if(!Watch) { Watch = MakeSpaceInBook(&WatchHandles.Handles); Watch->Files = InitBook(sizeof(watch_file), 16); Watch->TargetPath = TargetPath; Watch->WatchedPath = GetNearestExistingPath(TargetPath); // NOTE(matt): Stack-string char WatchablePath0[Watch->WatchedPath.Length + 1]; CopyStringNoFormat(WatchablePath0, sizeof(WatchablePath0), Watch->WatchedPath); Watch->Descriptor = inotify_add_watch(inotifyInstance, WatchablePath0, WatchHandles.DefaultEventsMask); //PrintWatchHandles(); } Result = PushWatchFileUniquely(Watch, Filename, Extension, Type, Project, Asset); return Result; } bool DescriptorIsRedundant(int Descriptor) { bool Result = TRUE; for(int i = 0; i < WatchHandles.Handles.ItemCount; ++i) { watch_handle *This = GetPlaceInBook(&WatchHandles.Handles, i); if(Descriptor == This->Descriptor) { string WatchablePath = GetNearestExistingPath(This->TargetPath); if(StringsMatch(This->WatchedPath, WatchablePath)) { Result = FALSE; break; } } } return Result; } int GetExistingWatchDescriptor(string Path) { int Result = -1; for(int i = 0; i < WatchHandles.Handles.ItemCount; ++i) { watch_handle *This = GetPlaceInBook(&WatchHandles.Handles, i); if(StringsMatch(Path, This->WatchedPath)) { Result = This->Descriptor; break; } } return Result; } void UpdateWatchHandles(int Descriptor) { for(int i = 0; i < WatchHandles.Handles.ItemCount; ++i) { watch_handle *This = GetPlaceInBook(&WatchHandles.Handles, i); if(Descriptor == This->Descriptor) { string WatchablePath = GetNearestExistingPath(This->TargetPath); if(StringsDiffer(This->WatchedPath, WatchablePath)) { if(DescriptorIsRedundant(Descriptor)) { inotify_rm_watch(inotifyInstance, This->Descriptor); } int NewDescriptor = GetExistingWatchDescriptor(WatchablePath); if(NewDescriptor == -1) { int NullTerminationBytes = 1; char WatchablePath0[WatchablePath.Length + NullTerminationBytes]; CopyStringNoFormat(WatchablePath0, sizeof(WatchablePath0), WatchablePath); NewDescriptor = inotify_add_watch(inotifyInstance, WatchablePath0, WatchHandles.DefaultEventsMask); } This->Descriptor = NewDescriptor; This->WatchedPath = WatchablePath; } } } } db_asset * LocateAsset(db_block_assets *Block, asset *Asset, int *Index) { db_asset *Result = 0; *Index = SAI_UNSET; if(!Block) { Block = InitBlock(B_ASET); } char *Ptr = (char *)Block; Ptr += sizeof(*Block); for(int i = 0; i < Block->Count; ++i) { db_asset *StoredAsset = (db_asset *)Ptr; string FilenameInDB = Wrap0i(StoredAsset->Filename); string FilenameInMemory = Wrap0i(Asset->Filename); if(StringsMatch(FilenameInDB, FilenameInMemory) && StoredAsset->Type == Asset->Type) { *Index = i; Result = StoredAsset; break; } Ptr = SkipHeaderedSectionRecursively(StoredAsset, GetDBStructureHeaderedSection(B_ASET)); } return Result; } void * LocateEndOfAssetsBlock(db_block_assets *Block) { char *Ptr = (char *)Block; Ptr += sizeof(*Block); for(int i = 0; i < Block->Count; ++i) { db_asset *Asset = (db_asset *)Ptr; Ptr += sizeof(*Asset) + sizeof(db_landmark) * Asset->LandmarkCount; } return Ptr; } void UpdateNeighbourhoodPointers(neighbourhood *N, file_signposts *S) { N->Project = S->ProjectHeader.Ptr; N->Prev = S->Prev.Ptr; N->This = S->This.Ptr; N->Next = S->Next.Ptr; } void WriteEntireDatabase(neighbourhood *N) { // NOTE(matt): This may suffice when the only changes are to existing fixed-size variables, // e.g. ProjectsBlock->GlobalSearchDir OpenFileForWriting(&DB.Metadata.File); fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle); CycleSignpostedFile(&DB.Metadata); UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts); } int UpdateAssetInDB(asset *Asset) { MEM_TEST_TOP(); int AssetIndexInDB = SAI_UNSET; if(Config->QueryString.Length > 0) { db_block_assets *AssetsBlock = LocateBlock(B_ASET); if(!AssetsBlock) { AssetsBlock = InitBlock(B_ASET); } DB.Metadata.Signposts.AssetsBlock.Ptr = AssetsBlock; db_asset *StoredAsset = LocateAsset(AssetsBlock, Asset, &AssetIndexInDB); if(StoredAsset) { if(StoredAsset->Associated != Asset->Associated || StoredAsset->Variants != Asset->Variants || StoredAsset->Width != Asset->Dimensions.Width || StoredAsset->Height != Asset->Dimensions.Height) { StoredAsset->Associated = Asset->Associated; StoredAsset->Variants = Asset->Variants; StoredAsset->Width = Asset->Dimensions.Width; StoredAsset->Height = Asset->Dimensions.Height; } if(StoredAsset->Hash != Asset->Hash) { // NOTE(matt): Extra-db code start char OldChecksum[16]; ClearCopyString(OldChecksum, sizeof(OldChecksum), "%08x", StoredAsset->Hash); char NewChecksum[16]; ClearCopyString(NewChecksum, sizeof(NewChecksum), "%08x", Asset->Hash); // NOTE(matt): Extra-db code end StoredAsset->Hash = Asset->Hash; // NOTE(matt): Extra-db code start file AssetFile = {}; AssetFile.Path = ConstructAssetPath(&AssetFile, Wrap0i(StoredAsset->Filename), StoredAsset->Type); ResolvePath(&AssetFile.Path); string MessageEditType = MakeString("sss", ColourStrings[CS_ONGOING], "Updating", ColourStrings[CS_END]); string Message = MakeString("sssssssssss", " checksum ", ColourStrings[CS_BLACK_BOLD], OldChecksum, ColourStrings[CS_END], " → ", ColourStrings[CS_BLUE_BOLD], NewChecksum, ColourStrings[CS_END], " of ", AssetFile.Path, " in HTML files"); Print(stderr, "%.*s%.*s", (int)MessageEditType.Length, MessageEditType.Base, (int)Message.Length, Message.Base); uint64_t MessageLength = MessageEditType.Length + Message.Length; if(SnipeChecksumIntoHTML(StoredAsset, Asset->Hash) == RC_SUCCESS) { ClearTerminalRow(MessageLength); PrintC(CS_REINSERTION, "Updated"); Print(stderr, "%.*s\n", (int)Message.Length, Message.Base); } FreeString(&Message); // NOTE(matt): Extra-db code end OpenFileForWriting(&DB.Metadata.File); fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle); SetFileEditPosition(&DB.Metadata); CycleSignpostedFile(&DB.Metadata); } Asset->DeferredUpdate = FALSE; } else { // Append new asset, not bothering to insertion sort because there likely won't be many OpenFileForWriting(&DB.Metadata.File); char *InsertionPoint = LocateEndOfAssetsBlock(AssetsBlock); AssetIndexInDB = AssetsBlock->Count; ++AssetsBlock->Count; uint64_t BytesIntoFile = InsertionPoint - DB.Metadata.File.Buffer.Location; fwrite(DB.Metadata.File.Buffer.Location, BytesIntoFile, 1, DB.Metadata.File.Handle); SetFileEditPosition(&DB.Metadata); db_asset StoredAsset = {}; StoredAsset.Hash = Asset->Hash; StoredAsset.Type = Asset->Type; StoredAsset.Variants = Asset->Variants; StoredAsset.Width = Asset->Dimensions.Width; StoredAsset.Height = Asset->Dimensions.Height; StoredAsset.Associated = Asset->Associated; ClearCopyStringNoFormatOrTerminate(StoredAsset.Filename, sizeof(StoredAsset.Filename), Wrap0i(Asset->Filename)); fwrite(&StoredAsset, sizeof(StoredAsset), 1, DB.Metadata.File.Handle); AccumulateFileEditSize(&DB.Metadata, sizeof(StoredAsset)); Print(stderr, "%sAppended%s %s asset: %s [%08x]\n", ColourStrings[CS_ADDITION], ColourStrings[CS_END], AssetTypeNames[StoredAsset.Type], StoredAsset.Filename, StoredAsset.Hash); fwrite(DB.Metadata.File.Buffer.Location + BytesIntoFile, DB.Metadata.File.Buffer.Size - BytesIntoFile, 1, DB.Metadata.File.Handle); CycleSignpostedFile(&DB.Metadata); } if(!Asset->Known) { Asset->Known = TRUE; } } MEM_TEST_END(); return AssetIndexInDB; } void PushAssetLandmark(buffer *Dest, asset *Asset, int PageType, bool GrowableBuffer) { if(Config->QueryString.Length > 0) { if(GrowableBuffer) { AppendStringToBuffer(Dest, Wrap0("?")); AppendStringToBuffer(Dest, Config->QueryString); AppendStringToBuffer(Dest, Wrap0("=")); } else { CopyStringToBuffer(Dest, "?%.*s=", (int)Config->QueryString.Length, Config->QueryString.Base); } if(PageType == PAGE_PLAYER) { landmark *This = MakeSpaceInBook(&Asset->Player); This->Offset = Dest->Ptr - Dest->Location; This->BufferID = Dest->ID; } else { landmark *This = MakeSpaceInBook(&Asset->Search); This->Offset = Dest->Ptr - Dest->Location; This->BufferID = Dest->ID; } if(GrowableBuffer) { // NOTE(matt): Stack-string char Hash[16] = {}; CopyString(Hash, sizeof(Hash), "%08x", Asset->Hash); AppendStringToBuffer(Dest, Wrap0(Hash)); } else { CopyStringToBuffer(Dest, "%08x", Asset->Hash); } } } void ResetAssetLandmarks(void) { for(int AssetIndex = 0; AssetIndex < Assets.ItemCount; ++AssetIndex) { asset *A = GetPlaceInBook(&Assets, AssetIndex); FreeAndReinitialiseBook(&A->Player); FreeAndReinitialiseBook(&A->Search); A->OffsetLandmarks = FALSE; } } vec2 GetImageDimensions(buffer *B) { vec2 Result = {}; stbi_info_from_memory(&*(stbi_uc *)B->Location, B->Size, (int *)&Result.Width, (int *)&Result.Height, 0); return Result; } int32_t UpdateAsset(asset *Asset, bool Defer) { int32_t AssetIndexInDB = SAI_UNSET; file File = {}; File.Path = ConstructAssetPath(&File, Wrap0i(Asset->Filename), Asset->Type); ReadFileIntoBuffer(&File); if(File.Buffer.Location) { Asset->Hash = StringToFletcher32(File.Buffer.Location, File.Buffer.Size); if(Asset->Type == ASSET_IMG) { Asset->Dimensions = GetImageDimensions(&File.Buffer); } if(!Defer) { AssetIndexInDB = UpdateAssetInDB(Asset); } else { Asset->DeferredUpdate = TRUE; } } else if(Asset->Associated) { AssetIndexInDB = UpdateAssetInDB(Asset); } FreeFile(&File, NA); return AssetIndexInDB; } void ComputeSpriteData(asset *A) { if(A->Variants) { sprite *S = &A->Sprite; bool HasLight = (A->Variants & (1 << AVS_LIGHT_NORMAL) || A->Variants & (1 << AVS_LIGHT_FOCUSED) || A->Variants & (1 << AVS_LIGHT_DISABLED)); bool HasDark = (A->Variants & (1 << AVS_DARK_NORMAL) || A->Variants & (1 << AVS_DARK_FOCUSED) || A->Variants & (1 << AVS_DARK_DISABLED)); int TileCountX = HasLight + HasDark; bool HasNormal = (A->Variants & (1 << AVS_LIGHT_NORMAL) || A->Variants & (1 << AVS_DARK_NORMAL)); bool HasFocused = (A->Variants & (1 << AVS_LIGHT_FOCUSED) || A->Variants & (1 << AVS_DARK_FOCUSED)); bool HasDisabled = (A->Variants & (1 << AVS_LIGHT_DISABLED) || A->Variants & (1 << AVS_DARK_DISABLED)); int TileCountY = HasNormal + HasFocused + HasDisabled; S->TileDim.Width = A->Dimensions.Width / TileCountX; S->TileDim.Height = A->Dimensions.Height / TileCountY; S->XLight = 0; S->XDark = 0; if(HasDark) { S->XDark += S->TileDim.Width * HasLight; } A->Sprite.YNormal = 0; A->Sprite.YFocused = 0; A->Sprite.YDisabled = 0; if(HasFocused) { S->YFocused -= S->TileDim.Height * HasNormal; } if(HasDisabled) { S->YDisabled -= S->TileDim.Height * (HasNormal + HasFocused); } if(!HasNormal && HasDisabled) { S->YNormal = S->YDisabled; } } } asset * PlaceAsset(string Filename, asset_type Type, uint64_t Variants, bool Associated, int Position) { asset *This = GetPlaceInBook(&Assets, Position); This->Type = Type; This->Variants = Variants; This->Associated = Associated; ClearCopyString(This->Filename, sizeof(This->Filename), "%.*s", (int)Filename.Length, Filename.Base); This->FilenameAt = FinalPathComponentPosition(Filename); if(Position == Assets.ItemCount && !This->Known) { This->Player = InitBook(sizeof(landmark), 8); This->Search = InitBook(sizeof(landmark), 8); ++Assets.ItemCount; } file File = {}; File.Path = ConstructAssetPath(&File, Filename, Type); ReadFileIntoBuffer(&File); if(File.Buffer.Location) { // TODO(matt): Print out the asset that we've hashed? This->Hash = StringToFletcher32(File.Buffer.Location, File.Buffer.Size); if(This->Type == ASSET_IMG) { This->Dimensions = GetImageDimensions(&File.Buffer); ComputeSpriteData(This); } PushWatchHandle(Wrap0(File.Path), EXT_NULL, WT_ASSET, 0, This); } else { ResolvePath(&File.Path); Print(stdout, "%sNonexistent%s %s asset: %s\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], AssetTypeNames[Type], File.Path); } FreeFile(&File, NA); return This; } asset * PushAsset(string Filename, asset_type Type, uint64_t Variants, bool Associated) { int i; for(i = 0; i < Assets.ItemCount; ++i) { asset *Asset = GetPlaceInBook(&Assets, i); if(!StringsDifferLv0(Filename, Asset->Filename) && Type == Asset->Type) { break; } } return PlaceAsset(Filename, Type, Variants, Associated, i); } void FreeAssets(memory_book *A) { for(int i = 0; i < A->ItemCount; ++i) { asset *Asset = GetPlaceInBook(&Assets, i); FreeAndReinitialiseBook(&Asset->Search); FreeAndReinitialiseBook(&Asset->Player); } FreeBook(A); } void InitBuiltinAssets(void) { // NOTE(matt): This places assets in their own known slots, letting people like GenerateTopicColours() index them directly Assert(BUILTIN_ASSETS_COUNT == ArrayCount(BuiltinAssets)); for(int AssetIndex = 0; AssetIndex < BUILTIN_ASSETS_COUNT; ++AssetIndex) { asset *This = BuiltinAssets + AssetIndex; if(!(PlaceAsset(Wrap0(This->Filename), This->Type, This->Variants, This->Associated, AssetIndex)) && AssetIndex == ASSET_CSS_TOPICS) { Print(stdout, " %s└───────┴────┴─── Don't worry about this one. We'll generate it if needed%s\n", ColourStrings[CS_COMMENT], ColourStrings[CS_END]); } } Assets.ItemCount = BUILTIN_ASSETS_COUNT; // TODO(matt): Think deeply about how and when we push these support icon assets on #if 0 Assert(SUPPORT_ICON_COUNT - BUILTIN_ASSETS_COUNT == ArrayCount(SupportIcons)); for(int SupportIconIndex = BUILTIN_ASSETS_COUNT; SupportIconIndex < SUPPORT_ICON_COUNT; ++SupportIconIndex) { PlaceAsset(Wrap0(SupportIcons[SupportIconIndex - BUILTIN_ASSETS_COUNT]), ASSET_IMG, SupportIconIndex); } #endif } void InitAssets(void) { Assets = InitBook(sizeof(asset), 16); InitBuiltinAssets(); db_block_assets *AssetsBlock = LocateBlock(B_ASET); if(AssetsBlock) { DB.Metadata.Signposts.AssetsBlock.Ptr = AssetsBlock; db_asset *Asset = LocateFirstAsset(AssetsBlock); for(int i = 0; i < AssetsBlock->Count; ++i) { PushAsset(Wrap0i(Asset->Filename), Asset->Type, Asset->Variants, Asset->Associated); Asset = SkipHeaderedSectionRecursively(Asset, GetDBStructureHeaderedSection(B_ASET)); } } } void ConstructResolvedAssetURL(buffer *Buffer, asset *Asset, page_type PageType) { ClaimBuffer(Buffer, BID_URL_ASSET, (MAX_ROOT_URL_LENGTH + 1 + MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1) * 2); ConstructURLPrefix(Buffer, Asset->Type, PageType); CopyStringToBufferHTMLPercentEncoded(Buffer, Wrap0i(Asset->Filename)); string BufferL = {}; BufferL.Base = Buffer->Location; BufferL.Length = Buffer->Ptr - Buffer->Location; char *ResolvablePath = MakeString0("l", &BufferL); ResolvePath(&ResolvablePath); RewindBuffer(Buffer); CopyStringToBuffer(Buffer, "%s", ResolvablePath); Free(ResolvablePath); } void PickAbbreviationScheme(speakers *Speakers) { Speakers->AbbrevScheme = AS_NONE; int SchemeCount = 3; for(int SchemeIndex = 0; SchemeIndex < SchemeCount; ++SchemeIndex) { bool Clash = FALSE; for(int i = 0; i < Speakers->Speakers.ItemCount; ++i) { speaker *A = GetPlaceInBook(&Speakers->Speakers, i); for(int j = i + 1; j < Speakers->Speakers.ItemCount; ++j) { speaker *B = GetPlaceInBook(&Speakers->Speakers, j); if(StringsMatch(A->Person->Abbreviations[SchemeIndex], B->Person->Abbreviations[SchemeIndex])) { Clash = TRUE; break; } } if(Clash) { break; } } if(!Clash) { Speakers->AbbrevScheme = SchemeIndex; break; } } } void SortSpeakersAndPickAbbreviationScheme(speakers *Speakers) { for(int i = 0; i < Speakers->Speakers.ItemCount; ++i) { speaker *A = GetPlaceInBook(&Speakers->Speakers, i); for(int j = i + 1; j < Speakers->Speakers.ItemCount; ++j) { speaker *B = GetPlaceInBook(&Speakers->Speakers, j); if(StringsDiffer(A->Person->ID, B->Person->ID) > 0) { person *Temp = B->Person; B->Person = A->Person; A->Person = Temp; break; } } } for(int i = 0; i < Speakers->Speakers.ItemCount; ++i) { speaker *This = GetPlaceInBook(&Speakers->Speakers, i); StringToColourHash(&This->Colour, This->Person->ID); } PickAbbreviationScheme(Speakers); } person * GetPersonFromConfig(string Person) { person *Result = 0; for(int i = 0; i < Config->Person.ItemCount; ++i) { person *This = GetPlaceInBook(&Config->Person, i); if(!StringsDifferCaseInsensitive(This->ID, Person)) { Result = This; break; } } return Result; } asset * GetAsset(string Filename, asset_type AssetType) { asset *Result = 0; for(int i = 0; i < Assets.ItemCount; ++i) { asset *This = GetPlaceInBook(&Assets, i); if(StringsMatch(Filename, Wrap0i(This->Filename)) && AssetType == This->Type) { Result = This; break; } } return Result; } void PushSupportIconAssets() { for(int i = 0; i < Config->Person.ItemCount; ++i) { person *Person = GetPlaceInBook(&Config->Person, i); for(int j = 0; j < Person->Support.ItemCount; ++j) { support *Support = GetPlaceInBook(&Person->Support, j); if(Support->IconType == IT_GRAPHICAL) { asset *IconAsset = GetAsset(Support->Icon, ASSET_IMG); if(!IconAsset) { IconAsset = PushAsset(Support->Icon, ASSET_IMG, Support->IconVariants, FALSE); } Support->IconAsset = IconAsset; } } } } typedef enum { AI_PROJECT_ART, AI_PROJECT_ICON, AI_ENTRY_ART, } associable_identifier; void * ConfirmAssociationsOfProject(db_header_project *Project, asset *Asset, db_asset *AssetInDB, int Index) { if(Project->ArtIndex == Index) { Asset->Associated = TRUE; AssetInDB->Associated = TRUE; } if(Project->IconIndex == Index) { Asset->Associated = TRUE; AssetInDB->Associated = TRUE; } db_entry *Entry = LocateFirstEntry(Project); for(int j = 0; j < Project->EntryCount; ++j, ++Entry) { if(Entry->ArtIndex == Index) { Asset->Associated = TRUE; AssetInDB->Associated = TRUE; } } db_header_project *Child = LocateFirstChildProject(Project); for(int ChildIndex = 0; ChildIndex < Project->ChildCount; ++ChildIndex) { Child = ConfirmAssociationsOfProject(Child, Asset, AssetInDB, Index); } return SkipHeaderedSectionRecursively(Child, GetDBStructureHeaderedSection(B_PROJ)); } void ConfirmAssociations(asset *Asset, db_asset *AssetInDB, int Index) { Asset->Associated = FALSE; AssetInDB->Associated = FALSE; db_block_projects *ProjectsBlock = LocateBlock(B_PROJ); db_header_project *Project = LocateFirstChildProjectOfBlock(ProjectsBlock); for(int i = 0; i < ProjectsBlock->Count; ++i) { Project = ConfirmAssociationsOfProject(Project, Asset, AssetInDB, Index); } } db_asset * LocateAssetByIndex(uint16_t Index) { db_asset *Result = 0; db_block_assets *AssetsBlock = LocateBlock(B_ASET); db_asset *This = LocateFirstAsset(AssetsBlock); for(int i = 0; i < AssetsBlock->Count; ++i) { if(Index == i) { Result = This; break; } This = SkipHeaderedSectionRecursively(This, GetDBStructureHeaderedSection(B_ASET)); } return Result; } asset * SyncAssetAssociation(string Path, uint64_t Variants, db_project_index Index, associable_identifier Type) { asset *Asset = GetAsset(Path, ASSET_IMG); if(!Asset) { Asset = PushAsset(Path, ASSET_IMG, Variants, TRUE); } Asset->Associated = TRUE; DB.Metadata.Signposts.ProjectHeader.Ptr = LocateProject(Index); int NewIndex = UpdateAsset(Asset, FALSE); db_header_project *ProjectInDB = DB.Metadata.Signposts.ProjectHeader.Ptr; int32_t *StoredIndex = 0; switch(Type) { case AI_PROJECT_ART: { StoredIndex = &ProjectInDB->ArtIndex; } break; case AI_PROJECT_ICON: { StoredIndex = &ProjectInDB->IconIndex; } break; default: Assert(0); break; } int OldIndex = *StoredIndex; *StoredIndex = NewIndex; if(OldIndex >= 0 && OldIndex != NewIndex) { db_asset *OldAssetInDB = LocateAssetByIndex(OldIndex); asset *OldAsset = GetAsset(Wrap0i(OldAssetInDB->Filename), OldAssetInDB->Type); ConfirmAssociations(OldAsset, OldAssetInDB, OldIndex); //DeleteStaleAssets(); // TODO(matt): Maybe do this later? } return Asset; } void PushMediumIconAsset(medium *Medium) { if(Medium->IconType == IT_GRAPHICAL) { asset *Asset = GetAsset(Medium->Icon, ASSET_IMG); if(!Asset) { Asset = PushAsset(Medium->Icon, ASSET_IMG, Medium->IconVariants, FALSE); } Medium->IconAsset = Asset; } } void PushProjectAssets(project *P) { string Theme = MakeString("sls", "cinera__", &P->Theme, ".css"); if(!GetAsset(Theme, ASSET_CSS)) { PushAsset(Theme, ASSET_CSS, CAV_DEFAULT_UNSET, FALSE); // NOTE(matt): This may want to be associated, if we change to // storing the Theme by index, rather than a string } if(P->Art.Length) { P->ArtAsset = SyncAssetAssociation(P->Art, P->ArtVariants, P->Index, AI_PROJECT_ART); } if(P->Icon.Length) { P->IconAsset = SyncAssetAssociation(P->Icon, P->IconVariants, P->Index, AI_PROJECT_ICON); } for(int i = 0; i < P->Medium.ItemCount; ++i) { medium *This = GetPlaceInBook(&P->Medium, i); PushMediumIconAsset(This); } for(int i = 0; i < P->Child.ItemCount; ++i) { PushProjectAssets(GetPlaceInBook(&P->Child, i)); } FreeString(&Theme); } void PushThemeAssets() { string GlobalTheme = MakeString("sls", "cinera__", &Config->GlobalTheme, ".css"); if(!GetAsset(GlobalTheme, ASSET_CSS)) { PushAsset(GlobalTheme, ASSET_CSS, CAV_DEFAULT_UNSET, FALSE); // NOTE(matt): This may want to be associated if we change to // storing the Theme by index rather than a string } FreeString(&GlobalTheme); for(int i = 0; i < Config->Project.ItemCount; ++i) { PushProjectAssets(GetPlaceInBook(&Config->Project, i)); } } void PushConfiguredAssets() { PushSupportIconAssets(); PushThemeAssets(); } // TODO(matt): Remove once we're ready to deprecate the old annotator=person style of crediting // char *RoleStrings[] = { "Cohost", "Guest", "Host", "Indexer", }; char *RoleIDStrings[] ={ "cohost", "guest", "host", "indexer", }; typedef enum { R_COHOST, R_GUEST, R_HOST, R_INDEXER, } role_id; // //// void PushIcon(buffer *Buffer, bool GrowableBuffer, icon_type IconType, string IconString, asset *IconAsset, uint64_t Variants, page_type PageType, bool *RequiresCineraJS) { if(IconType == IT_GRAPHICAL) { buffer AssetURL = {}; ConstructResolvedAssetURL(&AssetURL, IconAsset, PageType); if(Variants) { sprite *S = &IconAsset->Sprite; *RequiresCineraJS = TRUE; if(GrowableBuffer) { AppendStringToBuffer(Buffer, Wrap0("
Dimensions.Width, IconAsset->Dimensions.Height, S->TileDim.Width, S->TileDim.Height, S->XLight, S->XDark, S->YNormal, S->YFocused, S->YDisabled, AssetURL.Location); } PushAssetLandmark(Buffer, IconAsset, PageType, GrowableBuffer); if(GrowableBuffer) { AppendStringToBuffer(Buffer, Wrap0("\">
")); } else { CopyStringToBuffer(Buffer, "\">"); } } else { if(GrowableBuffer) { AppendStringToBuffer(Buffer, Wrap0("")); } else { CopyStringToBuffer(Buffer, "\">"); } } DeclaimBuffer(&AssetURL); } else { if(GrowableBuffer) { AppendStringToBuffer(Buffer, IconString); } else { CopyStringToBuffer(Buffer, "%.*s", (int)IconString.Length, IconString.Base); } } } speaker * GetSpeaker(memory_book *Speakers, string Username) { speaker *Result = 0; if(Username.Length > 0) { for(int i = 0; i < Speakers->ItemCount; ++i) { speaker *This = GetPlaceInBook(Speakers, i); if(!StringsDifferCaseInsensitive(This->Person->ID, Username)) { Result = This; break; } } } return Result; } void FreeSpeakers(speakers *Speakers) { FreeBook(&Speakers->Speakers); } void PushCredentials(buffer *CreditsMenu, memory_book *Speakers, person *Actor, role *Role, bool *RequiresCineraJS) { if(!Role->NonSpeaking) { speaker *This = GetSpeaker(Speakers, Actor->ID); if(!This) { This = MakeSpaceInBook(Speakers); This->Person = Actor; } } if(CreditsMenu->Ptr == CreditsMenu->Location) { CopyStringToBuffer(CreditsMenu, "
\n" " Credits\n" "
\n"); } CopyStringToBuffer(CreditsMenu, " \n"); string Name = Actor->Name.Length > 0 ? Actor->Name : Actor->ID; if(Actor->Homepage.Length) { CopyStringToBuffer(CreditsMenu, " \n" "
%.*s
\n" "
%.*s
\n" "
\n", (int)Actor->Homepage.Length, Actor->Homepage.Base, (int)Role->Name.Length, Role->Name.Base, (int)Name.Length, Name.Base); } else { CopyStringToBuffer(CreditsMenu, "
\n" "
%.*s
\n" "
%.*s
\n" "
\n", (int)Role->Name.Length, Role->Name.Base, (int)Name.Length, Name.Base); } // TODO(matt): Handle multiple support platforms! // AFD if(Actor->Support.ItemCount > 0) { support *Support = GetPlaceInBook(&Actor->Support, 0); CopyStringToBuffer(CreditsMenu, " ", (int)Support->URL.Length, Support->URL.Base); PushIcon(CreditsMenu, FALSE, Support->IconType, Support->Icon, Support->IconAsset, Support->IconVariants, PAGE_PLAYER, RequiresCineraJS); CopyStringToBuffer(CreditsMenu, "\n"); } CopyStringToBuffer(CreditsMenu, "
\n"); } void ErrorCredentials(string HMMLFilepath, string Actor, role *Role) { ErrorFilenameAndLineNumber(&HMMLFilepath, 0, S_ERROR, ED_INDEXING); Print(stderr, "No credentials for%s%s%.*s%s: %s%.*s%s\n", ColourStrings[CS_YELLOW_BOLD], Role ? " " : "", Role ? (int)Role->Name.Length : 0, Role ? Role->Name.Base : "", ColourStrings[CS_END], ColourStrings[CS_MAGENTA_BOLD], (int)Actor.Length, Actor.Base, ColourStrings[CS_END]); Print(stderr, "Perhaps you'd like to add a new person to your config file, e.g.:\n" " person = \"%.*s\"\n" " {\n" " name = \"Jane Doe\";\n" " homepage = \"https://example.com/\";\n" " support = \"their_support_platform\";\n" " }\n", (int)Actor.Length, Actor.Base); WaitForInput(); } void ErrorRole(string HMMLFilepath, string Role) { ErrorFilenameAndLineNumber(&HMMLFilepath, 0, S_ERROR, ED_INDEXING); if(Role.Length == 0) { Print(stderr, "Credit lacks a role\n"); Print(stderr, " A complete credit takes the form:\n" " credit = creditable_person:their_role\n"); } else { Print(stderr, "No such role: %s%.*s%s\n", ColourStrings[CS_YELLOW_BOLD], (int)Role.Length, Role.Base, ColourStrings[CS_END]); Print(stderr, "Perhaps you'd like to add a new role to your config file, e.g.:\n" " role = \"%.*s\"\n" " {\n" " name = \"Roller\";\n" " plural = \"Rollae\";\n" " position = -1;\n" " }\n", (int)Role.Length, Role.Base); } WaitForInput(); } typedef enum { MAT_NULL, MAT_CREDIT, MAT_CUSTOM, } misc_attribute_type; typedef struct { char *ID; misc_attribute_type Type; union { int CustomIndex; role_id RoleID; }; } misc_attribute; // TODO(matt): Migrate away from handling annotator / cohost / guest here misc_attribute MiscAttributes[] = { { "annotator", MAT_CREDIT, { R_INDEXER } }, { "co-host", MAT_CREDIT, { R_COHOST } }, { "guest", MAT_CREDIT, { R_GUEST } }, { "custom0", MAT_CUSTOM, { 0 } }, { "custom1", MAT_CUSTOM, { 1 } }, { "custom2", MAT_CUSTOM, { 2 } }, { "custom3", MAT_CUSTOM, { 3 } }, { "custom4", MAT_CUSTOM, { 4 } }, { "custom5", MAT_CUSTOM, { 5 } }, { "custom6", MAT_CUSTOM, { 6 } }, { "custom7", MAT_CUSTOM, { 7 } }, { "custom8", MAT_CUSTOM, { 8 } }, { "custom9", MAT_CUSTOM, { 9 } }, { "custom10", MAT_CUSTOM, { 10 } }, { "custom11", MAT_CUSTOM, { 11 } }, { "custom12", MAT_CUSTOM, { 12 } }, { "custom13", MAT_CUSTOM, { 13 } }, { "custom14", MAT_CUSTOM, { 14 } }, { "custom15", MAT_CUSTOM, { 15 } }, }; misc_attribute * GetMiscAttribute(HMML_VideoCustomMetaData *A) { misc_attribute *Result = 0; for(int i = 0; i < ArrayCount(MiscAttributes); ++i) { misc_attribute *This = MiscAttributes + i; if(!StringsDiffer0(This->ID, A->key)) { Result = This; break; } } return Result; } bool CreditsMatch(credit *A, credit *B) { return A->Person == B->Person && A->Role == B->Role; } credit * GetCredit(memory_book *Credits, credit *C) { credit *Result = 0; for(int i = 0; i < Credits->ItemCount; ++i) { credit *This = GetPlaceInBook(Credits, i); if(CreditsMatch(C, This)) { Result = This; break; } } return Result; } bool Uncredited(HMML_VideoMetaData *Metadata, credit *C) { bool Result = FALSE; for(int i = 0; i < Metadata->uncredit_count; ++i) { HMML_Credit *This = Metadata->uncredits + i; if(StringsMatch(C->Person->ID, Wrap0(This->name)) && StringsMatch(C->Role->ID, Wrap0(This->role))) { Result = TRUE; break; } } return Result; } bool IsValidCredit(credit *Credit) { return Credit->Person && Credit->Role; } void MergeCredits(memory_book *Credits, project *CurrentProject, HMML_VideoMetaData *Metadata) { // TODO(matt): Sort by SortName for(int ProjectCreditIndex = 0; ProjectCreditIndex < CurrentProject->Credit.ItemCount; ++ProjectCreditIndex) { credit *Credit = GetPlaceInBook(&CurrentProject->Credit, ProjectCreditIndex); if(IsValidCredit(Credit) && !Uncredited(Metadata, Credit) && !GetCredit(Credits, Credit)) { credit *New = MakeSpaceInBook(Credits); New->Person = Credit->Person; New->Role = Credit->Role; } } for(int CreditIndex = 0; CreditIndex < Metadata->credit_count; ++CreditIndex) { HMML_Credit *This = Metadata->credits + CreditIndex; credit Credit = {}; Credit.Person = GetPersonFromConfig(Wrap0(This->name)); Credit.Role = GetRoleByID(Config, Wrap0(This->role)); if(IsValidCredit(&Credit) && !Uncredited(Metadata, &Credit) && !GetCredit(Credits, &Credit)) { credit *New = MakeSpaceInBook(Credits); New->Person = Credit.Person; New->Role = Credit.Role; } } // TODO(matt): Delete this loop once we're ready to deprecate the old annotator=person style of crediting // for(int CustomIndex = 0; CustomIndex < Metadata->custom_count; ++CustomIndex) { HMML_VideoCustomMetaData *This = Metadata->custom + CustomIndex; misc_attribute *ThisAttribute = GetMiscAttribute(This); if(ThisAttribute && ThisAttribute->Type == MAT_CREDIT) { credit Credit = {}; Credit.Person = GetPersonFromConfig(Wrap0(This->value)); Credit.Role = GetRoleByID(Config, Wrap0(RoleIDStrings[ThisAttribute->RoleID])); if(IsValidCredit(&Credit) && !Uncredited(Metadata, &Credit) && !GetCredit(Credits, &Credit)) { credit *New = MakeSpaceInBook(Credits); New->Person = Credit.Person; New->Role = Credit.Role; } } } } int BuildCredits(string HMMLFilepath, buffer *CreditsMenu, HMML_VideoMetaData *Metadata, speakers *Speakers, bool *RequiresCineraJS) { memory_book Credits = InitBook(sizeof(credit), 4); MergeCredits(&Credits, CurrentProject, Metadata); for(int RoleIndex = 0; RoleIndex < Config->Role.ItemCount; ++RoleIndex) { role *Role = GetPlaceInBook(&Config->Role, RoleIndex); for(int CreditIndex = 0; CreditIndex < Credits.ItemCount; ++CreditIndex) { credit *This = GetPlaceInBook(&Credits, CreditIndex); if(!StringsDiffer(Role->ID, This->Role->ID)) { PushCredentials(CreditsMenu, &Speakers->Speakers, This->Person, Role, RequiresCineraJS); } } } FreeBook(&Credits); // NOTE(matt): As we only cite the speaker when there are a multiple of them, we only // need to SortSpeakersAndPickAbbreviationScheme() in the same situation if(Speakers->Speakers.ItemCount > 1) { SortSpeakersAndPickAbbreviationScheme(Speakers); } if(CreditsMenu->Ptr > CreditsMenu->Location) { CopyStringToBuffer(CreditsMenu, "
\n" "
\n"); } return RC_SUCCESS; } void InsertCategory(_memory_book(category_info) *GlobalTopics, _memory_book(category_info) *LocalTopics, _memory_book(category_info) *GlobalMedia, _memory_book(category_info) *LocalMedia, string Marker, hsl_colour *Colour) { medium *Medium = GetMediumFromProject(CurrentProject, Marker); if(Medium) { int MediumIndex; bool MadeLocalSpace = FALSE; for(MediumIndex = 0; MediumIndex < LocalMedia->ItemCount; ++MediumIndex) { category_info *This = GetPlaceInBook(LocalMedia, MediumIndex); if(StringsMatch(Medium->ID, This->Marker)) { return; } if((StringsDiffer(Medium->Name, This->WrittenText)) < 0) { MakeSpaceInBook(LocalMedia); MadeLocalSpace = TRUE; int CategoryCount; for(CategoryCount = LocalMedia->ItemCount - 1; CategoryCount > MediumIndex; --CategoryCount) { category_info *Src = GetPlaceInBook(LocalMedia, CategoryCount - 1); category_info *Dest = GetPlaceInBook(LocalMedia, CategoryCount); Dest->Marker = Src->Marker; Dest->WrittenText = Src->WrittenText; } category_info *New = GetPlaceInBook(LocalMedia, CategoryCount); New->Marker = Medium->ID; New->WrittenText = Medium->Name; break; } } if(!MadeLocalSpace) { MakeSpaceInBook(LocalMedia); } if(MediumIndex == LocalMedia->ItemCount - 1) { category_info *New = GetPlaceInBook(LocalMedia, MediumIndex); New->Marker = Medium->ID; New->WrittenText = Medium->Name; } bool MadeGlobalSpace = FALSE; for(MediumIndex = 0; MediumIndex < GlobalMedia->ItemCount; ++MediumIndex) { category_info *This = GetPlaceInBook(GlobalMedia, MediumIndex); if(StringsMatch(Medium->ID, This->Marker)) { return; } if((StringsDiffer(Medium->Name, This->WrittenText)) < 0) { MakeSpaceInBook(GlobalMedia); MadeGlobalSpace = TRUE; int CategoryCount; for(CategoryCount = GlobalMedia->ItemCount - 1; CategoryCount > MediumIndex; --CategoryCount) { category_info *Src = GetPlaceInBook(GlobalMedia, CategoryCount - 1); category_info *Dest = GetPlaceInBook(GlobalMedia, CategoryCount); Dest->Marker = Src->Marker; Dest->WrittenText = Src->WrittenText; } category_info *New = GetPlaceInBook(GlobalMedia, CategoryCount); New->Marker = Medium->ID; New->WrittenText = Medium->Name; break; } } if(!MadeGlobalSpace) { MakeSpaceInBook(GlobalMedia); } if(MediumIndex == GlobalMedia->ItemCount - 1) { category_info *New = GetPlaceInBook(GlobalMedia, MediumIndex); New->Marker = Medium->ID; New->WrittenText = Medium->Name; } } else { bool MadeLocalSpace = FALSE; int TopicIndex; for(TopicIndex = 0; TopicIndex < LocalTopics->ItemCount; ++TopicIndex) { category_info *This = GetPlaceInBook(LocalTopics, TopicIndex); if(StringsMatch(Marker, This->Marker)) { return; } if((StringsDiffer(Marker, This->Marker)) < 0) { int CategoryCount; MakeSpaceInBook(LocalTopics); MadeLocalSpace = TRUE; for(CategoryCount = LocalTopics->ItemCount - 1; CategoryCount > TopicIndex; --CategoryCount) { category_info *Src = GetPlaceInBook(LocalTopics, CategoryCount - 1); category_info *Dest = GetPlaceInBook(LocalTopics, CategoryCount); *Dest = *Src; } category_info *New = GetPlaceInBook(LocalTopics, CategoryCount); New->Marker = Marker; if(Colour) { New->Colour = *Colour; } break; } } if(!MadeLocalSpace) { MakeSpaceInBook(LocalTopics); } if(TopicIndex == LocalTopics->ItemCount - 1) { category_info *New = GetPlaceInBook(LocalTopics, TopicIndex); New->Marker = Marker; if(Colour) { New->Colour = *Colour; } } bool MadeGlobalSpace = FALSE; for(TopicIndex = 0; TopicIndex < GlobalTopics->ItemCount; ++TopicIndex) { category_info *This = GetPlaceInBook(GlobalTopics, TopicIndex); if(StringsMatch(Marker, This->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, This->Marker)) < 0 || StringsMatch(This->Marker, Wrap0("nullTopic")))) { if(StringsDiffer(Marker, Wrap0("nullTopic"))) // NOTE(matt): This test (with the above || condition) forces nullTopic never to be inserted, only appended { MakeSpaceInBook(GlobalTopics); MadeGlobalSpace = TRUE; int CategoryCount; for(CategoryCount = GlobalTopics->ItemCount - 1; CategoryCount > TopicIndex; --CategoryCount) { category_info *Src = GetPlaceInBook(GlobalTopics, CategoryCount - 1); category_info *Dest = GetPlaceInBook(GlobalTopics, CategoryCount); *Dest = *Src; } category_info *New = GetPlaceInBook(GlobalTopics, CategoryCount); New->Marker = Marker; if(Colour) { New->Colour = *Colour; } break; } } } if(!MadeGlobalSpace) { MakeSpaceInBook(GlobalTopics); } if(TopicIndex == GlobalTopics->ItemCount - 1) { category_info *New = GetPlaceInBook(GlobalTopics, TopicIndex); New->Marker = Marker; if(Colour) { New->Colour = *Colour; } } } } void BuildTimestampClass(buffer *TimestampClass, _memory_book(category_info) *LocalTopics, _memory_book(category_info) *LocalMedia, string DefaultMedium) { category_info *FirstLocalTopic = GetPlaceInBook(LocalTopics, 0); if(LocalTopics->ItemCount == 1 && StringsMatch(FirstLocalTopic->Marker, Wrap0("nullTopic"))) { // NOTE(matt): Stack-string char SanitisedMarker[FirstLocalTopic->Marker.Length + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)FirstLocalTopic->Marker.Length, FirstLocalTopic->Marker.Base); SanitisePunctuation(SanitisedMarker); CopyStringToBuffer(TimestampClass, " cat_%s", SanitisedMarker); } else { for(int i = 0; i < LocalTopics->ItemCount; ++i) { category_info *This = GetPlaceInBook(LocalTopics, i); // NOTE(matt): Stack-string char SanitisedMarker[This->Marker.Length + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base); SanitisePunctuation(SanitisedMarker); CopyStringToBuffer(TimestampClass, " cat_%s", SanitisedMarker); } } category_info *FirstLocalMedium = GetPlaceInBook(LocalMedia, 0); if(LocalMedia->ItemCount == 1 && StringsMatch(DefaultMedium, FirstLocalMedium->Marker)) { // NOTE(matt): Stack-string char SanitisedMarker[FirstLocalMedium->Marker.Length + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)FirstLocalMedium->Marker.Length, FirstLocalMedium->Marker.Base); SanitisePunctuation(SanitisedMarker); CopyStringToBuffer(TimestampClass, " %s", SanitisedMarker); } else { for(int i = 0; i < LocalMedia->ItemCount; ++i) { category_info *This = GetPlaceInBook(LocalMedia, i); // NOTE(matt): Stack-string char SanitisedMarker[This->Marker.Length + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base); SanitisePunctuation(SanitisedMarker); medium *Medium = GetMediumFromProject(CurrentProject, This->Marker); if(Medium) { if(Medium->Hidden) { CopyStringToBuffer(TimestampClass, " off_%s skip", SanitisedMarker); } else { CopyStringToBuffer(TimestampClass, " %s", SanitisedMarker); } } } } CopyStringToBuffer(TimestampClass, "\""); } void BuildCategoryIcons(buffer *CategoryIcons, _memory_book(category_info) *LocalTopics, _memory_book(category_info) *LocalMedia, string DefaultMedium, bool *RequiresCineraJS) { bool CategoriesSpan = FALSE; category_info *FirstLocalTopic = GetPlaceInBook(LocalTopics, 0); category_info *FirstLocalMedium = GetPlaceInBook(LocalMedia, 0); if(!(LocalTopics->ItemCount == 1 && StringsMatch(FirstLocalTopic->Marker, Wrap0("nullTopic")) && LocalMedia->ItemCount == 1 && StringsMatch(DefaultMedium, FirstLocalMedium->Marker))) { CategoriesSpan = TRUE; CopyStringToBuffer(CategoryIcons, ""); } if(!(LocalTopics->ItemCount == 1 && StringsMatch(FirstLocalTopic->Marker, Wrap0("nullTopic")))) { for(int i = 0; i < LocalTopics->ItemCount; ++i) { category_info *This = GetPlaceInBook(LocalTopics, i); // NOTE(matt): Stack-string char SanitisedMarker[This->Marker.Length + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base); SanitisePunctuation(SanitisedMarker); CopyStringToBuffer(CategoryIcons, "
Marker.Length, This->Marker.Base, SanitisedMarker); if(IsValidHSLColour(This->Colour)) { CopyStringToBuffer(CategoryIcons, " data-hue=\"%u\" data-saturation=\"%u%%\"", This->Colour.Hue, This->Colour.Saturation); } CopyStringToBuffer(CategoryIcons, ">
"); } } if(!(LocalMedia->ItemCount == 1 && StringsMatch(DefaultMedium, FirstLocalMedium->Marker))) { for(int i = 0; i < LocalMedia->ItemCount; ++i) { category_info *This = GetPlaceInBook(LocalMedia, i); // NOTE(matt): Stack-string char SanitisedMarker[This->Marker.Length + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base); SanitisePunctuation(SanitisedMarker); medium *Medium = GetMediumFromProject(CurrentProject, This->Marker); if(Medium && !Medium->Hidden) { CopyStringToBuffer(CategoryIcons, "
", (int)This->WrittenText.Length, This->WrittenText.Base, (int)This->Marker.Length, This->Marker.Base); PushIcon(CategoryIcons, FALSE, Medium->IconType, Medium->Icon, Medium->IconAsset, Medium->IconVariants, PAGE_PLAYER, RequiresCineraJS); CopyStringToBuffer(CategoryIcons, "
"); } } } if(CategoriesSpan) { CopyStringToBuffer(CategoryIcons, "
"); } } int64_t String0ToInt(char *String) { int64_t 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; }; CURLcode CurlQuotes(buffer *QuoteStaging, char *QuotesURL) { MEM_TEST_TOP(); Print(stderr, "%sFetching%s quotes: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], QuotesURL); CURLcode Result = CURLE_FAILED_INIT; CURL *curl = curl_easy_init(); if(curl) { curl_easy_setopt(curl, CURLOPT_WRITEDATA, &QuoteStaging->Ptr); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer); curl_easy_setopt(curl, CURLOPT_URL, QuotesURL); /* */ MEM_TEST_MID(); /* +MEM */ Result = curl_easy_perform(curl); /* */ MEM_TEST_MID(); if(Result) { Print(stderr, "%s\n", curl_easy_strerror(Result)); } curl_easy_cleanup(curl); curl = 0; } MEM_TEST_END(); return Result; } string GetStringFromBufferT(buffer *B, char Terminator) { // NOTE(matt): This just straight up assumes success // We may want to make it report a failure if, e.g. B->Ptr != Terminator after the loop string Result = { .Base = B->Ptr }; char *Ptr = B->Ptr; while(Ptr - B->Location < B->Size && *Ptr != Terminator) { ++Ptr; ++Result.Length; } return Result; } string UnixTimeToDateString(memory_book *Book, int64_t Time) { string Result = {}; // NOTE(matt): Stack-string char DayString[3] = { }; strftime(DayString, 3, "%e", gmtime(&Time)); Result = WriteStringInBook(Book, TrimWhitespace(Wrap0(DayString))); int Day = String0ToInt(DayString); // NOTE(matt): Stack-string if(DayString[1] == '1' && Day != 11) { Result = ExtendStringInBook(Book, Wrap0("st ")); } else if(DayString[1] == '2' && Day != 12) { Result = ExtendStringInBook(Book, Wrap0("nd ")); } else if(DayString[1] == '3' && Day != 13) { Result = ExtendStringInBook(Book, Wrap0("rd ")); } else { Result = ExtendStringInBook(Book, Wrap0("th ")); } // NOTE(matt): Stack-string char MonthYear[32] = {}; strftime(MonthYear, 32, "%B, %Y", gmtime(&Time)); Result = ExtendStringInBook(Book, Wrap0(MonthYear)); return Result; } rc SearchQuotes(memory_book *Strings, buffer *QuoteStaging, int CacheSize, quote_info *Info, int ID) { rc Result = RC_UNFOUND; QuoteStaging->Ptr = QuoteStaging->Location; while(QuoteStaging->Ptr - QuoteStaging->Location < CacheSize) { string InID = GetStringFromBufferT(QuoteStaging, ','); QuoteStaging->Ptr += InID.Length + 1; // Skip past the ',' if(StringToInt(InID) == ID) { string InTime = GetStringFromBufferT(QuoteStaging, ','); QuoteStaging->Ptr += InTime.Length + 1; // Skip past the ',' Info->Date = StringToInt(InTime); string Text = GetStringFromBufferT(QuoteStaging, '\n'); Info->Text = WriteStringInBook(Strings, Text); Result = RC_SUCCESS; break; } else { while(QuoteStaging->Ptr - QuoteStaging->Location < CacheSize && *QuoteStaging->Ptr != '\n') { ++QuoteStaging->Ptr; } ++QuoteStaging->Ptr; } } return Result; } rc BuildQuote(memory_book *Strings, quote_info *Info, string Speaker, int ID, bool ShouldFetchQuotes) { MEM_TEST_TOP(); rc Result = RC_SUCCESS; // TODO(matt): Suss out the Speaker more sensibly if(Speaker.Length > 0) { // TODO(matt): Generally sanitise this function, e.g. using MakeString0(), curling in to a growing buffer, etc. // NOTE(matt): Stack-string char QuoteCacheDir[256] = {}; CopyString(QuoteCacheDir, sizeof(QuoteCacheDir), "%.*s/quotes", (int)Config->CacheDir.Length, Config->CacheDir.Base); // NOTE(matt): Stack-string char QuoteCachePath[256] = {}; CopyString(QuoteCachePath, sizeof(QuoteCachePath), "%s/%.*s", QuoteCacheDir, (int)Speaker.Length, Speaker.Base); // NOTE(matt): Stack-string 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", (int)Speaker.Length, Speaker.Base); bool CacheAvailable = FALSE; FILE *QuoteCache = fopen(QuoteCachePath, "a+"); if(QuoteCache) { CacheAvailable = TRUE; } else { MakeDir(Wrap0i(QuoteCacheDir)); QuoteCache = fopen(QuoteCachePath, "a+"); if(QuoteCache) { CacheAvailable = TRUE; } else { // TODO(matt): SystemError(); Print(stderr, "Unable to open quote cache %s: %s\n", QuoteCachePath, strerror(errno)); } } buffer QuoteStaging = {}; QuoteStaging.ID = BID_QUOTE_STAGING; QuoteStaging.Size = Kilobytes(256); QuoteStaging.Location = malloc(QuoteStaging.Size); QuoteStaging.Ptr = QuoteStaging.Location; int CacheSize = 0; if(QuoteStaging.Location) { #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); WriteToFile(MemLog, " Allocated QuoteStaging (%ld)\n", QuoteStaging.Size); fclose(MemLog); Print(stdout, " Allocated QuoteStaging (%ld)\n", QuoteStaging.Size); #endif if(!ShouldFetchQuotes) { if(CacheAvailable) { fseek(QuoteCache, 0, SEEK_END); CacheSize = ftell(QuoteCache); fseek(QuoteCache, 0, SEEK_SET); fread(QuoteStaging.Location, CacheSize, 1, QuoteCache); fclose(QuoteCache); rc SearchQuotesResult = SearchQuotes(Strings, &QuoteStaging, CacheSize, Info, ID); if(SearchQuotesResult == RC_UNFOUND) { ShouldFetchQuotes = TRUE; } } else { ShouldFetchQuotes = TRUE; } } if(ShouldFetchQuotes) { QuoteStaging.Ptr = QuoteStaging.Location; /* */ MEM_TEST_MID(); /* +MEM */ CURLcode CurlQuotesResult = CurlQuotes(&QuoteStaging, QuotesURL); /* */ MEM_TEST_MID(); if(CurlQuotesResult == CURLE_OK) { LastQuoteFetch = time(0); CacheSize = QuoteStaging.Ptr - QuoteStaging.Location; Result = SearchQuotes(Strings, &QuoteStaging, CacheSize, Info, ID); } else { Result = RC_UNFOUND; } if(CacheAvailable) { QuoteCache = fopen(QuoteCachePath, "w"); fwrite(QuoteStaging.Location, CacheSize, 1, QuoteCache); fclose(QuoteCache); } } FreeBuffer(&QuoteStaging); } else { Result = RC_ERROR_MEMORY; if(CacheAvailable) { fclose(QuoteCache); } } MEM_TEST_END(); } return Result; } rc GenerateTopicColours(neighbourhood *N, string Topic, hsl_colour *Dest) { rc Result = RC_SUCCESS; // NOTE(matt): Stack-string char SanitisedTopic[Topic.Length + 1]; CopyString(SanitisedTopic, sizeof(SanitisedTopic), "%.*s", (int)Topic.Length, Topic.Base); SanitisePunctuation(SanitisedTopic); medium *Medium = GetMediumFromProject(CurrentProject, Topic); if(!Medium) { if(StringsMatch(Topic, Wrap0("nullTopic"))) { Dest->Hue = CINERA_HSL_TRANSPARENT_HUE; } else { StringToColourHash(Dest, Topic); } file Topics = {}; Topics.Path = 0; Topics.Buffer.ID = BID_TOPICS; if(Config->CSSDir.Length > 0) { Topics.Path = MakeString0("lslss", &Config->AssetsRootDir, "/", &Config->CSSDir, "/", BuiltinAssets[ASSET_CSS_TOPICS].Filename); } else { Topics.Path = MakeString0("lss", &Config->AssetsRootDir, "/", BuiltinAssets[ASSET_CSS_TOPICS].Filename); } char *Ptr = Topics.Path + StringLength(Topics.Path) - 1; while(*Ptr != '/') { --Ptr; } *Ptr = '\0'; DIR *CSSDirHandle = opendir(Topics.Path); // TODO(matt): open() if(!CSSDirHandle) { if(!MakeDir(Wrap0(Topics.Path))) { LogError(LOG_ERROR, "Unable to create directory %s: %s", Topics.Path, strerror(errno)); Print(stderr, "Unable to create directory %s: %s\n", Topics.Path, strerror(errno)); Result = RC_ERROR_DIRECTORY; }; } closedir(CSSDirHandle); if(Result == RC_SUCCESS) { *Ptr = '/'; ReadFileIntoBuffer(&Topics); bool Exists = FALSE; while(Topics.Buffer.Ptr - Topics.Buffer.Location < Topics.Buffer.Size) { Topics.Buffer.Ptr += sizeof(".category.")-1; if(!StringsDifferT(SanitisedTopic, Topics.Buffer.Ptr, ' ')) { Exists = TRUE; break; } while(Topics.Buffer.Ptr - Topics.Buffer.Location < Topics.Buffer.Size && *Topics.Buffer.Ptr != '\n') { ++Topics.Buffer.Ptr; } ++Topics.Buffer.Ptr; } if(!Exists) { Topics.Handle = fopen(Topics.Path, "a+"); if(Topics.Handle) { if(StringsMatch(Topic, Wrap0("nullTopic"))) { WriteToFile(Topics.Handle, ".category.%s { border: 1px solid transparent; background: transparent; }\n", SanitisedTopic); } else { WriteToFile(Topics.Handle, ".category.%s { border: 1px solid hsl(%d, %d%%, %d%%); background: hsl(%d, %d%%, %d%%); }\n", SanitisedTopic, Dest->Hue, Dest->Saturation, Dest->Lightness, Dest->Hue, Dest->Saturation, Dest->Lightness); } #if DEBUG_MEM MemLog = fopen("/home/matt/cinera_mem", "a+"); WriteToFile(MemLog, " Freed Topics (%ld)\n", Topics.Buffer.Size); fclose(MemLog); Print(stdout, " Freed Topics (%ld)\n", Topics.Buffer.Size); #endif CloseFile(&Topics, NA); asset *Asset = GetPlaceInBook(&Assets, ASSET_CSS_TOPICS); if(Asset->Known) { // NOTE(matt): We may index this out directly because InitBuiltinAssets() places it in its own known slot UpdateAsset(Asset, TRUE); UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts); } else { asset *CSSTopics = BuiltinAssets + ASSET_CSS_TOPICS; PlaceAsset(Wrap0(CSSTopics->Filename), CSSTopics->Type, CSSTopics->Variants, CSSTopics->Associated, ASSET_CSS_TOPICS); } } else { perror(Topics.Path); Result = RC_ERROR_FILE; } } } FreeFile(&Topics, NA); } return Result; } void ResetConfigIdentifierDescriptionDisplayedBools(void) { for(int i = 0; i < IDENT_COUNT; ++i) { ConfigIdentifiers[i].IdentifierDescriptionDisplayed = FALSE; ConfigIdentifiers[i].IdentifierDescription_MediumDisplayed = FALSE; ConfigIdentifiers[i].LocalVariableDescriptionDisplayed = FALSE; ConfigIdentifiers[i].IdentifierDescription_RoleDisplayed = FALSE; ConfigIdentifiers[i].IdentifierDescription_CreditDisplayed = FALSE; } } void PrintHelpConfig(void) { ResetConfigIdentifierDescriptionDisplayedBools(); // Config Syntax int IndentationLevel = 0; NewSection("Configuration", &IndentationLevel); NewSection("Assigning values", &IndentationLevel); PrintC(CS_YELLOW_BOLD, "identifier"); Print(stderr, " = "); PrintC(CS_GREEN_BOLD, "\"string\""); Print(stderr, ";"); IndentedCarriageReturn(IndentationLevel); PrintC(CS_YELLOW_BOLD, "identifier"); Print(stderr, " = "); PrintC(CS_BLUE_BOLD, "number"); Print(stderr, ";"); IndentedCarriageReturn(IndentationLevel); PrintC(CS_YELLOW_BOLD, "identifier"); Print(stderr, " = "); PrintC(CS_GREEN_BOLD, "\"boolean\""); Print(stderr, ";"); ++IndentationLevel; IndentedCarriageReturn(IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("(valid booleans: true, True, TRUE, yes, false, False, FALSE, no)")); --IndentationLevel; IndentedCarriageReturn(IndentationLevel); PrintC(CS_YELLOW_BOLD, "identifier"); Print(stderr, " = "); PrintC(CS_GREEN_BOLD, "\"scope\""); Print(stderr, " {"); IndentedCarriageReturn(IndentationLevel); Print(stderr, "}\n"); EndSection(&IndentationLevel); NewSection("Identifiers", &IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("We may set identifiers singly or multiple times in a given scope, as specified below for each identifier. \ If we set a \"single\" identifier multiple times then each setting overwrites the previous one, and Cinera will warn us. If we set a \"multi\" identifier multiple times \ then all of the settings will take effect.")); NewParagraph(IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("All settings get \"absorbed\" by child scopes, if that child scope may contain the identifier. For example, if we \ set base_dir at the root scope (i.e. not within a project scope) then the base_dir will automatically be set in all project scopes. Similarly if we set the owner in a project \ scope, then the owner will also be set in all child project scopes. Naturally we may need this setting to vary, while also wanting the concision of writing it once. The \ base_dir is a prime example of this. To facilitate this variance, we may use variables, notably the $lineage variable, as described below. A variable written in a setting at the root \ scope, which is absorbed by a project, only gets resolved as if it had been written in the project scope.")); memory_book TypeSpecs = InitTypeSpecs(); PrintTypeSpecs(&TypeSpecs, IndentationLevel); //FreeTypeSpecs(&TypeSpecs); EndSection(&IndentationLevel); Print(stderr, "\n"); NewSection("Variables", &IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("We write variables the same way as in shell scripts like bash, zsh, etc.")); ++IndentationLevel; IndentedCarriageReturn(IndentationLevel); PrintC(CS_CYAN, "$variable_name"); IndentedCarriageReturn(IndentationLevel); PrintC(CS_CYAN, "${variable_name_within_valid_variable_characters}"); --IndentationLevel; IndentedCarriageReturn(IndentationLevel); NewSection("Config local variables", &IndentationLevel); for(int i = 0; i < IDENT_COUNT; ++i) { if(ConfigIdentifiers[i].LocalVariableDescription && !ConfigIdentifiers[i].LocalVariableDescriptionDisplayed) { IndentedCarriageReturn(IndentationLevel); PrintStringC(CS_YELLOW_BOLD, Wrap0(ConfigIdentifiers[i].String)); ++IndentationLevel; IndentedCarriageReturn(IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0(ConfigIdentifiers[i].LocalVariableDescription)); ConfigIdentifiers[i].LocalVariableDescriptionDisplayed = TRUE; --IndentationLevel; } } EndSection(&IndentationLevel); Print(stderr, "\n"); NewSection("Environment variables", &IndentationLevel); TypesetString(INDENT_WIDTH *IndentationLevel, Wrap0("Run `export` to see all available environment variables")); EndSection(&IndentationLevel); EndSection(&IndentationLevel); // Variables Print(stderr, "\n"); NewSection("Miscellaneous", &IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("We may use C-style comments, single-line with // and multi-line with /* and */")); NewParagraph(IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("We may edit the config file(s) while Cinera is running, and it will pick up the changes.")); NewParagraph(IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("Tip: Since the syntax is very C-like, vim users may put the following line somewhere to \ enable syntax highlighting:")); NewSection(0, &IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("// vim:ft=c:")); EndSection(&IndentationLevel); EndSection(&IndentationLevel); Print(stderr, "\n"); #if 1 // numbering_scheme // linear // 1, 2, 3, ... // calendrical // 2020-02-07, 2020-03-08, 2020-12-25, ... // seasonal // S01E01 // Miscellaneous // // Defaults // NewSection("Defaults", &IndentationLevel); scope_tree *ScopeTree = InitRootScopeTree(); SetTypeSpec(ScopeTree, &TypeSpecs); SetDefaults(ScopeTree, &TypeSpecs); PrintScopeTree(ScopeTree, IndentationLevel); FreeScopeTree(ScopeTree); FreeTypeSpecs(&TypeSpecs); #endif Print(stderr, "\n"); } #define PrintHelp() PrintHelp_(Args[0], ConfigPath) void PrintHelp_(char *BinaryLocation, char *DefaultConfigPath) { // Options Print(stderr, "Usage: %s [option(s)]\n" "\n" "Options:\n" " -c \n" " Set the main config file path\n" " Defaults to: %s\n" " -0\n" " Dry-run mode. Parse and print the config, but do not modify the filesystem.\n" " -e\n" " Display (examine) database and exit\n" " -v\n" " Display version and exit\n" " -h\n" " Display this help\n", BinaryLocation, DefaultConfigPath); PrintHelpConfig(); } void PrintHelp_Old(char *BinaryLocation) { #if AFE Print(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) { Print(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) { Print(stderr, "\t"); } Print(stderr, "%s\n", ProjectInfo[ProjectIndex].Medium); } Print(stderr, "%s -s