#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%.*} hmml.a -lcurl gcc -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl #clang -fsanitize=address -g -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl #clang -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl 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 = 7, .Patch = 10 }; #include // NOTE(matt): varargs #include // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror #include // NOTE(matt): calloc, malloc, free #include "hmmlib.h" #include // NOTE(matt): getopts #include #include #include #include #include #include // NOTE(matt): strerror #include //NOTE(matt): errno #include // NOTE(matt): inotify #include // NOTE(matt): ioctl and TIOCGWINSZ #include #define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW #include // NOTE(matt): open() #define __USE_XOPEN2K // NOTE(matt): readlink() #include // NOTE(matt): sleep() #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 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 0 #define DEBUG_MEM 0 // //// bool PROFILING = 0; clock_t TIMING_START; #define START_TIMING_BLOCK(...) if(PROFILING) { printf(__VA_ARGS__); TIMING_START = clock(); } #define END_TIMING_BLOCK() if(PROFILING) { printf("\e[1;34m%ld\e[0m\n", clock() - TIMING_START);} #define Kilobytes(Bytes) Bytes << 10 #define Megabytes(Bytes) Bytes << 20 #define MAX_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 32 #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_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)){ fprintf(stderr, "l.%d: \e[1;31mAssertion failure\e[0m\n", __LINE__); __asm__("int3"); } } while(0) #define BreakHere() fprintf(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 FreeAndResetCountAndCapacity(M, Count, Capacity) { FreeAndResetCount(M, Count); Capacity = 0; } #define MIN(A, B) A < B ? A : B void Clear(void *V, uint64_t Size) { char *Ptr = (char *)V; for(int i = 0; i < Size; ++i) { *Ptr++ = 0; } } void * Fit(void *Base, uint16_t WidthInBytes, uint64_t ItemCount, uint32_t ItemsPerBlock, bool ZeroInitialise) { if(ItemCount % ItemsPerBlock == 0) { Base = realloc(Base, (ItemCount + ItemsPerBlock) * WidthInBytes); if(ZeroInitialise) { Clear(Base + WidthInBytes * ItemCount, WidthInBytes * ItemsPerBlock); } } return Base; } void * FitShrinkable(void *Base, uint16_t WidthInBytes, uint64_t ItemCount, uint64_t *Capacity, uint32_t ItemsPerBlock, bool ZeroInitialise) { if(ItemCount == *Capacity) { Base = realloc(Base, (ItemCount + ItemsPerBlock) * WidthInBytes); if(ZeroInitialise) { Clear(Base + WidthInBytes * ItemCount, WidthInBytes * ItemsPerBlock); } *Capacity += ItemsPerBlock; } return Base; } // 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) \ fprintf(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*/) { fprintf(stderr, "iter %2i: %i (%s%i)\n", i, MemLoopNew, MemLoopNew > MemLoopOld ? "+" : "-", MemLoopNew - MemLoopOld); sleep(1); }\ #define MEM_LOOP_POST(String) \ MemLoopOld = MemLoopNew;\ }\ fprintf(stderr, "Done (%s): ", String);\ Colourise(MemLoopNew > MemLoopInitial ? CS_RED : CS_GREEN);\ fprintf(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 #define MEM_TEST_INITIAL() int Initial = GetUsage() #define MEM_TEST_MID(String) if(GetUsage() > Initial) fprintf(stderr, "%s: %i kb\n", String, GetUsage() - Initial) #define MEM_TEST_AFTER(String) int Current = GetUsage();\ Colourise(Current > Initial ? CS_RED : CS_BLUE);\ fprintf(stderr, "%s leaks %i kb\n", String, Current - Initial);\ Colourise(CS_END); #else #define MEM_TEST_INITIAL() #define MEM_TEST_MID(String) #define MEM_TEST_AFTER(String) #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_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_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; } file; typedef struct { char *Base; uint64_t Length; } string; int CopyStringToBarePtr(char *Dest, string Src) { for(int i = 0; i < Src.Length; ++i) { *Dest++ = Src.Base[i]; } return Src.Length; } 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; typedef enum { MBT_NONE, MBT_ASSET, MBT_MEDIUM, MBT_NAVIGATION_BUFFER, MBT_PERSON, MBT_PROJECT, MBT_STRING, MBT_SUPPORT, MBT_TAG_OFFSET, MBT_TOKEN, MBT_TOKENS, } memory_book_type; typedef struct { int64_t PageCount; uint64_t PageSize; uint64_t DataWidthInBytes; uint64_t ItemCount; memory_page *Pages; memory_pen_location *Pen; memory_book_type Type; } memory_book; #define _memory_book(type) memory_book void InitBook(memory_book *M, uint64_t DataWidthInBytes, uint64_t ItemsPerPage, memory_book_type Type) { M->PageSize = ItemsPerPage * DataWidthInBytes; M->Type = Type; M->DataWidthInBytes = DataWidthInBytes; } 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 FreeBook(memory_book *M) { for(int i = 0; i < M->PageCount; ++i) { FreePage(&M->Pages[i]); } FreeAndResetCount(M->Pages, M->PageCount); if(M->Type == MBT_STRING) { memory_pen_location *This = M->Pen; while(This) { M->Pen = M->Pen->Prev; Free(This); This = M->Pen; } } memory_book Zero = {}; *M = Zero; } void FreeAndReinitialiseBook(memory_book *M) { int PageSize = M->PageSize; int Type = M->Type; FreeBook(M); M->PageSize = PageSize; M->Type = Type; } 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; } 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; } 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 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); } void PrintPage(memory_page *P) { fprintf(stderr, "%.*s\n\n", (int)(P->Ptr - P->Base), P->Base); } void PrintBook(memory_book *M) { switch(M->Type) { case MBT_STRING: { for(int i = 0; i < M->PageCount; ++i) { PrintPage(&M->Pages[i]); } } break; case MBT_NONE: Assert(0); break; case MBT_ASSET: Assert(0); break; case MBT_MEDIUM: Assert(0); break; case MBT_NAVIGATION_BUFFER: Assert(0); break; case MBT_PERSON: Assert(0); break; case MBT_PROJECT: Assert(0); break; case MBT_SUPPORT: Assert(0); break; case MBT_TAG_OFFSET: Assert(0); break; case MBT_TOKEN: Assert(0); break; case MBT_TOKENS: Assert(0); break; } } 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; } uint64_t StringLength(char *String) { uint64_t i = 0; if(String) { while(String[i]) { ++i; } } return i; } 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) { //fprintf(stderr, "%c vs %c\n", *A, *B); while(*A && *B && *A == *B) { //fprintf(stderr, "%c vs %c\n", *A, *B); //fprintf(stderr, "[%d] %c\n", Location, *A); ++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 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; } 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; } string Wrap0(char *String) { string Result = {}; Result.Base = String; Result.Length = StringLength(String); return Result; } 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; } 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) { 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]); } return Result; } rc ReadFileIntoBuffer(file *F) { rc Result = RC_ERROR_FILE; if(F->Path) { if((F->Handle = fopen(F->Path, "r"))) { 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); fclose(F->Handle); F->Handle = 0; Result = RC_SUCCESS; } else { perror(F->Path); } } return Result; } 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) { fprintf(stderr, "%s", ColourStrings[C]); } void PrintString(string S) { //fprintf(stderr, "PrintString()\n"); fprintf(stderr, "%.*s", (int)S.Length, S.Base); } void PrintStringN(string S) { //fprintf(stderr, "PrintString()\n"); fprintf(stderr, "\n%.*s", (int)S.Length, S.Base); } void PrintStringI(string S, uint64_t Indentation) { for(int i = 0; i < Indentation; ++i) { fprintf(stderr, " "); } PrintString(S); } void PrintStringC(colour_code Colour, string String) { Colourise(Colour); PrintString(String); Colourise(CS_END); } void PrintC(colour_code Colour, char *String) { Colourise(Colour); fprintf(stderr, "%s", String); Colourise(CS_END); } #if DEBUG_PRINT_FUNCTION_NAMES void PrintFunctionName(char *N) { PrintC(CS_MAGENTA, N); fprintf(stderr, "\n"); } void PrintLinedFunctionName(int LineNumber, char *N) { Colourise(CS_BLACK_BOLD); fprintf(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_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", "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; bool IdentifierDescriptionDisplayed; bool IdentifierDescription_MediumDisplayed; bool LocalVariableDescriptionDisplayed; } config_identifier; config_identifier ConfigIdentifiers[] = { { "" }, { "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. There is no way to \"uncredit\" people in a HMML file. If a person ought not be credited for the whole project, just add them in \ video node of the entries for which they should be credited." }, { "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_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. There is no way to \"uncredit\" people in a HMML file. If a person ought not be credited for the whole project, just add them in \ video node of the entries for which they should be credited." }, { "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. There is no way to \"uncredit\" people in a HMML file. If a person ought not be credited for the whole project, just add them in \ video node of the entries for which they should be credited." }, { "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." }, { "numbering_scheme", "Possible numbering schemes: \"calendrical\", \"linear\", \"seasonal\". Only \"linear\" (the default) is treated specially. We assume that .hmml file names take the form: \ \"$project$episode_number.hmml\". Under the \"linear\" scheme, Cinera tries to derive each entry's number in its project by skipping past the project ID, then replacing all underscores with full \ stops. This derived number is then used in the search results to denote the entry." }, { "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: owner, cohost, guest, indexer).", 0, "The ID of the person within whose 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." }, { "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" }, { "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." }, { "stream_username", "We use this username to retrieve quotes from insobot. If it is not set, we use the host's ID when contacting insobot. The purpose of this setting is to let us identify project owners in one way, \ perhaps to automatically construct paths, while recognising that same person when they stream under a different username." }, { "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" }, { "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." }, { "unit", "This works in conjunction with the numbering_scheme. It 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." }, { "vod_platform", "This is a setting more for the future. We currently only support \"youtube\" for the purposes of generating the player page. But an additional use of the vod_platform is as \ a template tag (see also Templating)." }, { "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_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_CSS_PATH, IDENT_DB_LOCATION, 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_JS_PATH, IDENT_LINEAGE, IDENT_LINEAGE_WITHOUT_ORIGIN, IDENT_LOG_LEVEL, IDENT_MEDIUM, IDENT_NAME, IDENT_NUMBERING_SCHEME, IDENT_ORIGIN, IDENT_OWNER, IDENT_PERSON, IDENT_PLAYER_LOCATION, IDENT_PLAYER_TEMPLATE, IDENT_PRIVACY_CHECK_INTERVAL, IDENT_PROJECT, IDENT_QUERY_STRING, IDENT_SEARCH_LOCATION, IDENT_SEARCH_TEMPLATE, IDENT_SINGLE_BROWSER_TAB, IDENT_STREAM_PLATFORM, IDENT_STREAM_USERNAME, IDENT_SUPPORT, 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_UNIT, IDENT_VOD_PLATFORM, IDENT_URL, IDENT_COUNT, } config_identifier_id; void Indent(uint64_t Indent) { for(int i = 0; i < INDENT_WIDTH * Indent; ++i) { fprintf(stderr, " "); } } void IndentedCarriageReturn(int IndentationLevel) { fprintf(stderr, "\n"); Indent(IndentationLevel); } void NewParagraph(int IndentationLevel) { fprintf(stderr, "\n"); IndentedCarriageReturn(IndentationLevel); } void NewSection(char *Title, int *IndentationLevel) { IndentedCarriageReturn(*IndentationLevel); if(Title) { fprintf(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) { fprintf(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) { fprintf(stderr, "\n"); for(int i = 0; i < CurrentColumn; ++i) { fprintf(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; uint64_t Mapping:63; uint64_t Unused:1; } 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; typedef enum { S_ERROR, S_WARNING, } severity; void ConfigErrorFilenameAndLineNumber(char *Filename, uint64_t LineNumber, severity Severity) { fprintf(stderr, "\n" "┌─ "); switch(Severity) { case S_ERROR: PrintC(CS_ERROR, "Config error"); break; case S_WARNING: PrintC(CS_WARNING, "Config warning"); break; } if(Filename) { fprintf(stderr, " on line "); Colourise(CS_BLUE_BOLD); fprintf(stderr, "%lu", LineNumber); Colourise(CS_END); fprintf(stderr, " of "); PrintC(CS_CYAN, Filename); } fprintf(stderr, "\n" "└─╼ "); } // TODO(matt): Get more errors going through Error() void ConfigError(char *Filename, uint64_t LineNumber, severity Severity, char *Message, string *Received) { ConfigErrorFilenameAndLineNumber(Filename, LineNumber, Severity); fprintf(stderr, "%s", Message); if(Received) { PrintStringC(CS_MAGENTA_BOLD, *Received); } fprintf(stderr, "\n"); } void ConfigErrorUnset(config_identifier_id FieldID) { ConfigErrorFilenameAndLineNumber(0, 0, S_ERROR); fprintf(stderr, "Unset %s\n", ConfigIdentifiers[FieldID].String); } void ConfigFileIncludeError(char *Filename, uint64_t LineNumber, string Path) { ConfigErrorFilenameAndLineNumber(Filename, LineNumber, S_WARNING); fprintf(stderr, "Included file could not be opened (%s): ", strerror(errno)); PrintStringC(CS_MAGENTA_BOLD, Path); fprintf(stderr, "\n"); } void ConfigErrorSizing(char *Filename, uint64_t LineNumber, config_identifier_id FieldID, string *Received, uint64_t MaxSize) { ConfigErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR); fprintf(stderr, "%s value is too long (%lu/%lu characters): ", ConfigIdentifiers[FieldID].String, Received->Length, MaxSize); PrintStringC(CS_MAGENTA_BOLD, *Received); fprintf(stderr, "\n"); } void ConfigErrorInt(char *Filename, uint64_t LineNumber, severity Severity, char *Message, uint64_t Number) { ConfigErrorFilenameAndLineNumber(Filename, LineNumber, Severity); fprintf(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); ConfigErrorFilenameAndLineNumber(T->File.Path, This->LineNumber, S_ERROR); fprintf(stderr, "Syntax error: Received "); if(This->Content.Base) { PrintStringC(CS_RED, This->Content); } else { fprintf(stderr, "%s%ld%s", ColourStrings[CS_BLUE_BOLD], This->int64_t, ColourStrings[CS_END]); } fprintf(stderr, " but Expected "); PrintStringC(CS_GREEN, Wrap0(TokenStrings[GreaterExpectation])); if(LesserExpectation) { fprintf(stderr, " or "); PrintStringC(CS_GREEN, Wrap0(TokenStrings[LesserExpectation])); } fprintf(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(char *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); fprintf(stderr, " Valid log levels:\n"); for(int i = 0; i < LOG_COUNT; ++i) { fprintf(stderr, " %s\n", LogLevelStrings[i]); } return LOG_COUNT; } void FreeBuffer(buffer *Buffer) { Free(Buffer->Location); Buffer->Ptr = 0; Buffer->Size = 0; //Buffer->ID = 0; Buffer->IndentLevel = 0; } 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(char *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); fprintf(stderr, " Valid genres:\n"); for(int i = 0; i < GENRE_COUNT; ++i) { fprintf(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(char *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); fprintf(stderr, " Valid numbering schemes:\n"); for(int i = 0; i < NS_COUNT; ++i) { fprintf(stderr, " %s\n", NumberingSchemeStrings[i]); } return NS_COUNT; } bool GetBoolFromString(char *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); fprintf(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 IsNumber(char C) { return (C >= '0' && C <= '9'); } int64_t ParseArtVariantsString(char *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) { fprintf(stderr, " Valid variant_types:\n"); int FirstValidVariant = 1; for(int i = FirstValidVariant; i < CAV_COUNT; ++i) { fprintf(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(char *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); fprintf(stderr, " Valid icon_types:\n"); int FirstValidIconType = 1; for(int i = FirstValidIconType; i < IT_COUNT; ++i) { fprintf(stderr, " %s\n", IconTypeStrings[i]); } return NS_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 { unsigned int PrevStart, NextStart; unsigned short int PrevEnd, NextEnd; } link_insertion_offsets; // NOTE(matt): PrevStart is Absolute (or relative to start of file), the others are Relative to PrevStart typedef struct { link_insertion_offsets LinkOffsets; unsigned short int Size; char BaseFilename[MAX_BASE_FILENAME_LENGTH + 1]; char Title[MAX_TITLE_LENGTH + 1]; } db_entry3; typedef struct { file 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]; // TODO(matt): Replace this with the OutputPath, when we add that } db_header_entries4; typedef db_entry3 db_entry4; typedef struct { uint32_t BlockID; // 'ASET' uint16_t Count; char RootDir[MAX_ROOT_DIR_LENGTH]; 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 Reserved[46]; } 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; int32_t 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 EntryIndex; uint32_t Position; } db_landmark5; typedef struct { file File; file_signposted Metadata; 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 // TODO(matt): Increment CINERA_DB_VERSION! 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]; int32_t 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; uint32_t SearchLandmarkCount; landmark *Search; uint32_t PlayerLandmarkCount; 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_search.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_SEARCH, 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_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_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) { Template->Metadata.Validity = 0; Template->Metadata.Type = TEMPLATE_NULL; Template->Metadata.RequiresCineraJS = FALSE; FreeTemplateNavBuffers(Template); FreeBook(&Template->Metadata.NavBuffer); } void FreeFile(file *F) { FreeBuffer(&F->Buffer); Free(F->Path); F->Handle = 0; } void FreeSignpostedFile(file_signposted *F) { FreeFile(&F->File); file_signposts Zero = {}; F->Signposts = Zero; } void FreeTemplate(template *Template) { FreeFile(&Template->File); FreeBook(&Template->Metadata.Tags); 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; Result.Length -= (CharsFromStart + CharsFromEnd); Result.Base += CharsFromStart; return Result; } char *WatchTypeStrings[] = { "WT_HMML", "WT_ASSET", "WT_CONFIG", }; typedef enum { WT_HMML, WT_ASSET, WT_CONFIG, } watch_type; #include "cinera_config.c" typedef struct { watch_type Type; string Path; extension_id Extension; project *Project; asset *Asset; } watch_file; typedef struct { int Descriptor; string WatchedPath; string TargetPath; uint64_t FileCount; uint64_t FileCapacity; watch_file *Files; } watch_handle; typedef struct { uint64_t Count; uint64_t Capacity; watch_handle *Handles; memory_book Paths; uint32_t DefaultEventsMask; } watch_handles; #define AFD 1 #define AFE 0 // NOTE(matt): Globals 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 // 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; 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; } } fprintf(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 fprintf(LogFile, "\n"); fclose(LogFile); Free(LogPath); } } 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]; char Custom1[MAX_CUSTOM_SNIPPET_SHORT_LENGTH]; char Custom2[MAX_CUSTOM_SNIPPET_SHORT_LENGTH]; char Custom3[MAX_CUSTOM_SNIPPET_SHORT_LENGTH]; char Custom4[MAX_CUSTOM_SNIPPET_SHORT_LENGTH]; char Custom5[MAX_CUSTOM_SNIPPET_SHORT_LENGTH]; char Custom6[MAX_CUSTOM_SNIPPET_SHORT_LENGTH]; char Custom7[MAX_CUSTOM_SNIPPET_SHORT_LENGTH]; char Custom8[MAX_CUSTOM_SNIPPET_SHORT_LENGTH]; char Custom9[MAX_CUSTOM_SNIPPET_SHORT_LENGTH]; char Custom10[MAX_CUSTOM_SNIPPET_SHORT_LENGTH]; char Custom11[MAX_CUSTOM_SNIPPET_SHORT_LENGTH]; char Custom12[MAX_CUSTOM_SNIPPET_LONG_LENGTH]; char Custom13[MAX_CUSTOM_SNIPPET_LONG_LENGTH]; char Custom14[MAX_CUSTOM_SNIPPET_LONG_LENGTH]; char Custom15[MAX_CUSTOM_SNIPPET_LONG_LENGTH]; char Title[MAX_TITLE_LENGTH]; char URLSearch[MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH]; char URLPlayer[MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_BASE_FILENAME_LENGTH]; char VideoID[MAX_VOD_ID_LENGTH]; char VODPlatform[16]; } buffers; int 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; printf(" ClaimBuffer(%s): %d\n" " Total ClaimedMemory: %ld (%.2f%%, leaving %ld free)\n\n", Buffer->ID, Buffer->Size, MemoryArena.Ptr - MemoryArena.Location, PercentageUsed, MemoryArena.Size - (MemoryArena.Ptr - MemoryArena.Location)); #endif return RC_SUCCESS; } void DeclaimBuffer(buffer *Buffer) { if(Buffer->Size > 0) { float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100; #if DEBUG printf("DeclaimBuffer(%s)\n" " Used: %ld / %d (%.2f%%)\n" "\n" " Total ClaimedMemory: %ld\n\n", Buffer->ID, Buffer->Ptr - Buffer->Location, Buffer->Size, PercentageUsed, MemoryArena.Ptr - MemoryArena.Location); #endif LogUsage(Buffer); if(PercentageUsed >= 95.0f) { // TODO(matt): Implement either dynamically growing buffers, or phoning home to miblodelcarpio@gmail.com LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", BufferIDStrings[Buffer->ID], PercentageUsed); fprintf(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 miblodelcarpio@gmail.com LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", BufferIDStrings[Buffer->ID], PercentageUsed); fprintf(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; printf("Rewinding %s\n" " Used: %ld / %d (%.2f%%)\n\n", 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; // TODO(matt): Consider putting the ref_info and quote_info into linked lists on the heap, just to avoid all the hardcoded sizes typedef struct { char Date[32]; char Text[512]; } quote_info; typedef struct { char Timecode[8]; int Identifier; } identifier; #define MAX_REF_IDENTIFIER_COUNT 64 typedef struct { char RefTitle[620]; char ID[512]; char URL[512]; char Source[256]; identifier Identifier[MAX_REF_IDENTIFIER_COUNT]; int IdentifierCount; } ref_info; typedef struct { char Marker[32]; char WrittenText[32]; } category_info; typedef struct { category_info Category[64]; int Count; } categories; typedef struct { char *Name; colour_code Colour; } edit_type; typedef enum { EDIT_INSERTION, EDIT_APPEND, EDIT_REINSERTION, EDIT_DELETION, EDIT_ADDITION, } edit_type_id; edit_type EditTypes[] = { { "Inserted", CS_ADDITION }, { "Appended", CS_ADDITION }, { "Reinserted", CS_REINSERTION }, { "Deleted", CS_DELETION }, { "Added", CS_ADDITION } }; #define CopyString(Dest, DestSize, Format, ...) CopyString_(__LINE__, (Dest), (DestSize), (Format), ##__VA_ARGS__) __attribute__ ((format (printf, 4, 5))) int CopyString_(int LineNumber, char Dest[], int DestSize, char *Format, ...) { int Length = 0; va_list Args; va_start(Args, Format); Length = vsnprintf(Dest, DestSize, Format, Args); if(Length >= DestSize) { printf("CopyString() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %d(+1)-character string\n", LineNumber, DestSize, Length); __asm__("int3"); } va_end(Args); return Length; } #define 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) { printf("CopyString() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %d(+1)-character string\n", LineNumber, DestSize, Length); __asm__("int3"); } va_end(Args); return Length; } #define CopyStringNoFormat(Dest, DestSize, String) CopyStringNoFormat_(__LINE__, Dest, DestSize, String) int CopyStringNoFormat_(int LineNumber, char *Dest, int DestSize, string String) { if(String.Length + 1 > DestSize) { printf("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"); } for(int i = 0; i < String.Length; ++i) { *Dest++ = String.Base[i]; } *Dest = '\0'; return String.Length; } #define ClearCopyStringNoFormat(Dest, DestSize, String) ClearCopyStringNoFormat_(__LINE__, Dest, DestSize, String) int ClearCopyStringNoFormat_(int LineNumber, char *Dest, int DestSize, string String) { if(String.Length + 1 > DestSize) { printf("ClearCopyStringNoFormat() 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"); } Clear(Dest, DestSize); for(int i = 0; i < String.Length; ++i) { *Dest++ = String.Base[i]; } *Dest = '\0'; return 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 Length = 0; char *Start = String; while(*String != Terminator) { *Dest++ = *String++; ++Length; } if(Length >= DestSize) { printf("CopyStringNoFormatT() call on line %d has been passed a buffer too small (%d bytes) to contain %c-terminated %d(+1)-character string:\n" "%.*s\n", LineNumber, DestSize, Terminator == 0 ? '0' : Terminator, Length, Length, Start); __asm__("int3"); } *Dest = '\0'; return Length; } #define CopyStringToBuffer(Dest, Format, ...) CopyStringToBuffer_(__LINE__, Dest, Format, ##__VA_ARGS__) __attribute__ ((format (printf, 3, 4))) void CopyStringToBuffer_(int LineNumber, buffer *Dest, char *Format, ...) { va_list Args; va_start(Args, Format); int Length = vsnprintf(Dest->Ptr, Dest->Size - (Dest->Ptr - Dest->Location), Format, Args); va_end(Args); if(Length + (Dest->Ptr - Dest->Location) >= Dest->Size) { fprintf(stderr, "CopyStringToBuffer(%s) call on line %d cannot accommodate null-terminated %d(+1)-character string:\n" "%s\n", BufferIDStrings[Dest->ID], LineNumber, Length, Format); __asm__("int3"); } Dest->Ptr += Length; } #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)) { fprintf(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"); } for(int i = 0; i < String.Length; ++i) { *Dest->Ptr++ = String.Base[i]; } *Dest->Ptr = '\0'; } #define CopyStringToBufferNoFormatL(Dest, Length, String) CopyStringToBufferNoFormatL_(__LINE__, Dest, Length, String) void CopyStringToBufferNoFormatL_(int LineNumber, buffer *Dest, int Length, char *String) { char *Start = String; for(int i = 0; i < Length; ++i) { *Dest->Ptr++ = *String++; } if(Dest->Ptr - Dest->Location >= Dest->Size) { fprintf(stderr, "CopyStringToBufferNoFormat(%s) call on line %d cannot accommodate %ld-character string:\n" "%s\n", BufferIDStrings[Dest->ID], LineNumber, StringLength(Start), Start); __asm__("int3"); } *Dest->Ptr = '\0'; } #define CopyStringToBufferHTMLSafe(Dest, String) CopyStringToBufferHTMLSafe_(__LINE__, Dest, String) void CopyStringToBufferHTMLSafe_(int LineNumber, buffer *Dest, 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) { fprintf(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 CopyStringToBufferHTMLSafeBreakingOnSlash(Dest, String) CopyStringToBufferHTMLSafeBreakingOnSlash_(__LINE__, Dest, String) void CopyStringToBufferHTMLSafeBreakingOnSlash_(int LineNumber, buffer *Dest, char *String) { char *Start = String; int Length = StringLength(String); while(*String) { switch(*String) { case '<': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'l'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break; case '>': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'g'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break; case '&': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'a'; *Dest->Ptr++ = 'm'; *Dest->Ptr++ = 'p'; *Dest->Ptr++ = ';'; Length += 4; break; case '\'': *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = '9'; *Dest->Ptr++ = ';'; Length += 4; break; case '\"': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'q'; *Dest->Ptr++ = 'u'; *Dest->Ptr++ = 'o'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 5; break; case '/': *Dest->Ptr++ = '/'; *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '8'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '0'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = ';'; Length += 7; break; default: *Dest->Ptr++ = *String; break; } ++String; } if(Dest->Ptr - Dest->Location >= Dest->Size) { fprintf(stderr, "CopyStringToBufferHTMLSafeBreakingOnSlash(%s) call on line %d cannot accommodate %d(+1)-character HTML-sanitised string:\n" "%s\n", BufferIDStrings[Dest->ID], LineNumber, Length, Start); __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) { fprintf(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) { fprintf(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"); } for(int i = 0; i < Src->Ptr - Src->Location; ++i) { *Dest->Ptr++ = Src->Location[i]; } *Dest->Ptr = '\0'; } 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->PlayerLandmarkCount; ++LandmarkIndex) { if(Asset->Player[LandmarkIndex].BufferID == Src->ID) { Asset->Player[LandmarkIndex].Offset += Dest->Ptr - Dest->Location; Asset->Player[LandmarkIndex].BufferID = Dest->ID; } } } else { for(int LandmarkIndex = 0; LandmarkIndex < Asset->SearchLandmarkCount; ++LandmarkIndex) { if(Asset->Search[LandmarkIndex].BufferID == Src->ID) { Asset->Search[LandmarkIndex].Offset += Dest->Ptr - Dest->Location; Asset->Search[LandmarkIndex].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) { fprintf(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); for(int i = 0; i < Src->Ptr - Src->Location; ++i) { *Dest->Ptr++ = Src->Location[i]; } *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) { fprintf(stderr, "CopyBufferSized(%s) call on line %d cannot accommodate %ld-character %s\n", BufferIDStrings[Dest->ID], LineNumber, Size, BufferIDStrings[Src->ID]); __asm__("int3"); } for(int i = 0; i < Size; ++i) { *Dest->Ptr++ = Src->Location[i]; } } 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)); } 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); wordexp(WorkingPath, &Expansions, Flags); 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); 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 = GetPlaceInBook(&Template->Metadata.Tags, Template->Metadata.Tags.ItemCount); This->Offset = Offset; This->TagCode = TagCode; This->Asset = Asset; This->Nav = NavBuffer; ++Template->Metadata.Tags.ItemCount; } 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); } else { if(Location.Base[0] == '/') { Template->File = InitFile(0, &Location, EXT_NULL); } else { Template->File = InitFile(Type == TEMPLATE_BESPOKE ? &CurrentProject->HMMLDir : &CurrentProject->TemplatesDir, &Location, EXT_NULL); } } fprintf(stderr, "%sPacking%s template: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], Template->File.Path); ReadFileIntoBuffer(&Template->File); ClearTemplateMetadata(Template); InitBook(&Template->Metadata.Tags, sizeof(tag_offset), 16, MBT_TAG_OFFSET); InitBook(&Template->Metadata.NavBuffer, sizeof(navigation_buffer), 4, MBT_NAVIGATION_BUFFER); } int TimecodeToSeconds(char *Timecode) { int HMS[3] = { 0, 0, 0 }; // 0 == Seconds; 1 == Minutes; 2 == Hours int Colons = 0; while(*Timecode) { //if((*Timecode < '0' || *Timecode > '9') && *Timecode != ':') { return FALSE; } if(*Timecode == ':') { ++Colons; //if(Colons > 2) { return FALSE; } for(int i = 0; i < Colons; ++i) { HMS[Colons - i] = HMS[Colons - (i + 1)]; } HMS[0] = 0; } else { HMS[0] = HMS[0] * 10 + *Timecode - '0'; } ++Timecode; } //if(HMS[0] > 59 || HMS[1] > 59 || Timecode[-1] == ':') { return FALSE; } return HMS[2] * 60 * 60 + HMS[1] * 60 + HMS[0]; } typedef struct { unsigned int Hue:16; unsigned int Saturation:8; unsigned int Lightness:8; } hsl_colour; hsl_colour CharToColour(char Char) { hsl_colour Colour; if(Char >= 'a' && Char <= 'z') { Colour.Hue = (((float)Char - 'a') / ('z' - 'a') * 360); Colour.Saturation = (((float)Char - 'a') / ('z' - 'a') * 26 + 74); } else if(Char >= 'A' && Char <= 'Z') { Colour.Hue = (((float)Char - 'A') / ('Z' - 'A') * 360); Colour.Saturation = (((float)Char - 'A') / ('Z' - 'A') * 26 + 74); } else if(Char >= '0' && Char <= '9') { Colour.Hue = (((float)Char - '0') / ('9' - '0') * 360); Colour.Saturation = (((float)Char - '0') / ('9' - '0') * 26 + 74); } else { Colour.Hue = 180; Colour.Saturation = 50; } return Colour; } void StringToColourHash(hsl_colour *Colour, 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; } 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; } } // TODO(matt): Do a PushSpeaker() type of thing typedef struct { hsl_colour Colour; person *Person; char *Abbreviation; bool Seen; } speaker; typedef struct { speaker *Speaker; uint64_t Count; } speakers; enum { CreditsError_NoHost, CreditsError_NoIndexer, CreditsError_NoCredentials } credits_errors; size_t StringToFletcher32(const char *Data, size_t Length) {// https://en.wikipedia.org/wiki/Fletcher%27s_checksum size_t c0, c1; unsigned short int i; for(c0 = c1 = 0; Length >= 360; Length -= 360) { for(i = 0; i < 360; ++i) { c0 += *Data++; c1 += c0; } c0 %= 65535; c1 %= 65535; } for(i = 0; i < Length; ++i) { c0 += *Data++; c1 += c0; } c0 %= 65535; c1 %= 65535; return (c1 << 16 | c0); } 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) { fclose(File->Handle); // TODO(matt): Rather than freeing the buffer, why not just realloc it to fit the new file? Couldn't that work easily? // The reason we Free / Reread is to save us having to shuffle the buffer contents around, basically // If we switch the whole database over to use linked lists - the database being the only file we actually cycle // at the moment - then we may actually obviate the need to cycle at all FreeBuffer(&File->Buffer); ReadFileIntoBuffer(File); } 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 fclose(File->File.Handle); // TODO(matt): Rather than freeing the buffer, why not just realloc it to fit the new file? Couldn't that work easily? // The reason we Free / Reread is to save us having to shuffle the buffer contents around, basically // If we switch the whole database over to use linked lists - the database being the only file we actually cycle // at the moment - then we may actually obviate the need to cycle at all 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) { fprintf(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->Project, Landmark->Project) && FirstLandmarkOfRange->EntryIndex == Landmark->EntryIndex) { ++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->Project, Landmark->Project) && LandmarkInRange->EntryIndex == Landmark->EntryIndex) { --Result.First; --Landmark; } if(LandmarkInRange->EntryIndex != Landmark->EntryIndex) { ++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->EntryIndex) { Result.First = Lower; Result.Length = 0; return Result; } // TODO(matt): Is there a slicker way of doing this? if(EntryIndex > UpperLandmark->EntryIndex) { 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->EntryIndex) { return GetIndexRange(Asset, ProjectRange, Lower); } if(EntryIndex == PivotLandmark->EntryIndex) { return GetIndexRange(Asset, ProjectRange, Pivot); } if(EntryIndex == UpperLandmark->EntryIndex) { return GetIndexRange(Asset, ProjectRange, Upper); } if(EntryIndex < PivotLandmark->EntryIndex) { Upper = Pivot; } else { Lower = Pivot; } Pivot = Upper - ((Upper - Lower) >> 1); } while(Upper > Pivot); Result.First = Upper; Result.Length = 0; return Result; } return Result; } void PrintEntryIndex(db_project_index Project, int64_t EntryIndex) { fprintf(stderr, "%s%i:%i%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) { PrintEntryIndex(L->Project, L->EntryIndex); fprintf(stderr, " %6u", L->Position); } void PrintAsset(db_asset *A, uint16_t *Index) { if(Index) { Colourise(CS_BLACK_BOLD); fprintf(stderr, "[%i]", *Index); Colourise(CS_END); } string FilenameL = Wrap0i(A->Filename, sizeof(A->Filename)); fprintf(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) { fprintf(stderr, " "); } } fprintf(stderr, " Associated: "); if(Associated) { PrintC(CS_GREEN, "true"); } else { PrintC(CS_RED, "false"); } fprintf(stderr, " • Variants: "); Colourise(CS_BLUE_BOLD); fprintf(stderr, "%lu", Variants); Colourise(CS_END); fprintf(stderr, " • Dimensions: "); Colourise(CS_BLUE_BOLD); fprintf(stderr, "%u", A->Width); fprintf(stderr, "×"); fprintf(stderr, "%u", A->Height); Colourise(CS_END); fprintf(stderr, "\n"); } } void PrintAssetAndLandmarks(db_asset *A, uint16_t *Index) { fprintf(stderr, "\n" "\n"); PrintAsset(A, Index); db_landmark *FirstLandmark = LocateFirstLandmark(A); //Colourise(CS_BLACK_BOLD); fprintf(stderr, "%4u ", 0); Colourise(CS_END); //PrintLandmark(FirstLandmark); for(int i = 0; i < A->LandmarkCount; ++i) { db_landmark *This = FirstLandmark + i; if((i % 8) == 0) { fprintf(stderr, "\n"); } else { PrintC(CS_BLACK_BOLD, " │ "); } Colourise(CS_BLACK_BOLD); fprintf(stderr, "%4u ", i); Colourise(CS_END); PrintLandmark(This); } fprintf(stderr, "\n"); } // TODO(matt): Almost definitely redo this using Locate*() functions... void SnipeChecksumAndCloseFile(file *HTMLFile, db_asset *Asset, int LandmarksInFile, buffer *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; CopyBufferSized(&HTMLFile->Buffer, Checksum, Checksum->Ptr - Checksum->Location); } HTMLFile->Handle = fopen(HTMLFile->Path, "w"); fwrite(HTMLFile->Buffer.Location, HTMLFile->Buffer.Size, 1, HTMLFile->Handle); fclose(HTMLFile->Handle); HTMLFile->Handle = 0; FreeFile(HTMLFile); } // TODO(matt): Bounds-check the Metadata.File.Buffer void * SkipAsset(db_asset *Asset) { char *Ptr = (char *)Asset; Ptr += sizeof(db_asset) + sizeof(db_landmark) * Asset->LandmarkCount; return Ptr; } void * SkipAssetsBlock(db_block_assets *Block) { char *Ptr = (char *)Block; Ptr += sizeof(db_block_assets); for(int i = 0; i < Block->Count; ++i) { db_asset *Asset = (db_asset *)Ptr; Ptr = SkipAsset(Asset); } return Ptr; } typedef struct { uint64_t CurrentGeneration; uint64_t Count; uint32_t *EntriesInGeneration; } project_generations; void PushGeneration(project_generations *G) { G->EntriesInGeneration = Fit(G->EntriesInGeneration, sizeof(*G->EntriesInGeneration), G->Count, 4, TRUE); ++G->Count; } db_project_index GetCurrentProjectIndex(project_generations *G) { db_project_index Result = {}; Result.Generation = G->CurrentGeneration; Result.Index = G->EntriesInGeneration[G->CurrentGeneration]; return Result; } void AddEntryToGeneration(project_generations *G, project *P) { if(G) { if(G->Count <= G->CurrentGeneration) { PushGeneration(G); } if(P) { P->Index = GetCurrentProjectIndex(G); } ++G->EntriesInGeneration[G->CurrentGeneration]; } } 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->Count; ++i) { fprintf(stderr, "%lu: %u%s\n", i, G->EntriesInGeneration[i], IndicateCurrentGeneration && i == G->CurrentGeneration ? " [Current Generation]" : ""); } } void FreeGenerations(project_generations *G) { FreeAndResetCount(G->EntriesInGeneration, G->Count); } void * SkipProject(db_header_project *Project) { char *Ptr = (char *)Project; Ptr += sizeof(db_header_project) + sizeof(db_entry) * Project->EntryCount; return Ptr; } void * SkipProjectAndChildren(db_header_project *Project) { db_header_project *Ptr = SkipProject(Project); for(int i = 0; i < Project->ChildCount; ++i) { Ptr = SkipProjectAndChildren(Ptr); } return Ptr; } void * SkipProjectsBlock(db_block_projects *Block) { char *Ptr = (char *)Block; Ptr += sizeof(db_block_projects); for(int i = 0; i < Block->Count; ++i) { db_header_project *Project = (db_header_project *)Ptr; Ptr = SkipProjectAndChildren(Project); } return Ptr; } typedef enum { B_ASET, B_PROJ, } block_id; uint32_t GetFourFromBlockID(block_id ID) { switch(ID) { case B_ASET: return FOURCC("ASET"); case B_PROJ: return FOURCC("PROJ"); } return 0; } void * SkipBlock(void *Block) { void *Result = 0; uint32_t FirstInt = *(uint32_t *)Block; if(FirstInt == GetFourFromBlockID(B_PROJ)) { Result = SkipProjectsBlock(Block); } else if(FirstInt == GetFourFromBlockID(B_ASET)) { Result = SkipAssetsBlock(Block); } Assert(Result); 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 *Child = LocateFirstChildProject(Header); for(int i = 0; i < Header->ChildCount; ++i) { if(Accumulator->Generation == DesiredProject->Generation) { if(Accumulator->Index == DesiredProject->Index) { return Child; } else { ++Accumulator->Index; } } ++Accumulator->Generation; db_header_project *Test = LocateProjectRecursively(Child, DesiredProject, Accumulator); if(Test) { return Test; } --Accumulator->Generation; Child = SkipProjectAndChildren(Child); } return 0; } db_header_project * LocateProject(db_project_index Project) { 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) { if(Project.Generation == Accumulator.Generation) { for(int i = 0; i < ProjectsBlock->Count; ++i) { if(Project.Index == Accumulator.Index) { return Child; } Child = SkipProjectAndChildren(Child); ++Accumulator.Index; } } else { ++Accumulator.Generation; for(int i = 0; i < ProjectsBlock->Count; ++i) { db_header_project *Test = LocateProjectRecursively(Child, &Project, &Accumulator); if(Test) { return Test; } Child = SkipProjectAndChildren(Child); } --Accumulator.Generation; } } } return 0; } db_entry * LocateFirstEntry(db_header_project *P) { char *Ptr = (char *)P; Ptr += sizeof(db_header_project); return (db_entry *)Ptr; } db_entry * LocateEntry(db_project_index DBProjectIndex, int32_t EntryIndex) { db_header_project *Project = LocateProject(DBProjectIndex); if(Project && EntryIndex < Project->EntryCount) { char *Ptr = (char *)Project; Ptr += sizeof(db_header_project) + sizeof(db_entry) * EntryIndex; db_entry *Result = (db_entry *)Ptr; return Result; } return 0; } 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; } // 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; DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); 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 fprintf(stderr, "[%d] ", LineNumber); PrintFunctionName("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 = SkipAsset(A); } 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); } rc ReadGlobalSearchPageIntoBuffer(file *File) { db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr ? DB.Metadata.Signposts.ProjectsBlock.Ptr : LocateBlock(B_PROJ); string SearchLocationL = Wrap0i(ProjectsBlock->GlobalSearchDir, sizeof(ProjectsBlock->GlobalSearchDir)); 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, buffer *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->Project); file HTML = {}; rc FileReadRC = RC_INIT; if(Landmark->EntryIndex >= 0) { db_entry *Entry = LocateEntry(Landmark->Project, Landmark->EntryIndex); if(Entry) { string BaseDir = Wrap0i(P->BaseDir, sizeof(P->BaseDir)); string PlayerLocation = Wrap0i(P->PlayerLocation, sizeof(P->PlayerLocation)); string OutputLocation = Wrap0i(Entry->OutputLocation, sizeof(Entry->OutputLocation)); FileReadRC = ReadPlayerPageIntoBuffer(&HTML, BaseDir, PlayerLocation, OutputLocation); } else { PrintC(CS_ERROR, "\nInvalid landmark (aborting update): "); PrintAsset(Asset, 0); PrintLandmark(Landmark); Result = RC_FAILURE; break; } } else { switch(Landmark->EntryIndex) { case SP_SEARCH: { if(P) { string BaseDir = Wrap0i(P->BaseDir, sizeof(P->BaseDir)); string SearchLocation = Wrap0i(P->SearchLocation, sizeof(P->SearchLocation)); FileReadRC = ReadSearchPageIntoBuffer(&HTML, &BaseDir, &SearchLocation); } else { FileReadRC = ReadGlobalSearchPageIntoBuffer(&HTML); } } break; default: { Colourise(CS_RED); fprintf(stderr, "SnipeChecksumIntoHTML() does not know about special page with index %i\n", Landmark->EntryIndex); 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) { fprintf(stderr, "%s", WatchTypeStrings[W->Type]); fprintf(stderr, " • "); if(W->Extension != EXT_NULL) { fprintf(stderr, "*"); PrintString(ExtensionStrings[W->Extension]); } else { PrintString(W->Path); } if(W->Project) { fprintf(stderr, " • "); PrintLineage(W->Project->Lineage, FALSE); } if(W->Asset) { fprintf(stderr, " • "); fprintf(stderr, "%s", AssetTypeNames[W->Asset->Type]); } } void PrintWatchHandle(watch_handle *W) { fprintf(stderr, "%4d", W->Descriptor); fprintf(stderr, " • "); if(StringsDiffer(W->TargetPath, W->WatchedPath)) { PrintStringC(CS_MAGENTA, W->WatchedPath); fprintf(stderr, "\n "); PrintStringC(CS_RED_BOLD, W->TargetPath); } else { PrintStringC(CS_GREEN_BOLD, W->TargetPath); } for(int i = 0; i < W->FileCount; ++i) { watch_file *This = W->Files + i; fprintf(stderr, "\n" " "); PrintWatchFile(This); } } #define PrintWatchHandles() PrintWatchHandles_(__LINE__) void PrintWatchHandles_(int LineNumber) { typography T = { .UpperLeftCorner = "┌", .UpperLeft = "╾", .Horizontal = "─", .UpperRight = "╼", .Vertical = "│", .LowerLeftCorner = "└", .LowerLeft = "╽", .Margin = " ", .Delimiter = ": ", .Separator = "•", }; fprintf(stderr, "\n" "%s%s%s [%i] PrintWatchHandles()", T.UpperLeftCorner, T.Horizontal, T.UpperRight, LineNumber); for(int i = 0; i < WatchHandles.Count; ++i) { watch_handle *This = WatchHandles.Handles + i; fprintf(stderr, "\n"); PrintWatchHandle(This); } fprintf(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; } void PushWatchFileUniquely(watch_handle *Handle, string Filepath, extension_id Extension, watch_type Type, project *Project, asset *Asset) { bool Required = TRUE; for(int i = 0; i < Handle->FileCount; ++i) { watch_file *This = Handle->Files + i; if(This->Type == Type) { if(Extension != EXT_NULL) { if(This->Extension == Extension) { Required = FALSE; break; } } else { if(StringsMatch(This->Path, Filepath)) { Required = FALSE; EraseCurrentStringFromBook(&WatchHandles.Paths); break; } } } } if(Required) { Handle->Files = FitShrinkable(Handle->Files, sizeof(*Handle->Files), Handle->FileCount, &Handle->FileCapacity, 16, TRUE); watch_file *New = Handle->Files + Handle->FileCount; if(Extension != EXT_NULL) { New->Extension = Extension; } else { New->Path = Filepath; } New->Type = Type; New->Project = Project; New->Asset = Asset; ++Handle->FileCount; } } 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)) { //fprintf(stderr, "%s\n", OriginalPath0); int PathLength = readlink(OriginalPath0, ResolvedSymlinkPath, 4096); FullPath = Wrap0i(ResolvedSymlinkPath, PathLength); //PrintString(FullPath); //fprintf(stderr, "\n"); } 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; } void PushWatchHandle(string Path, extension_id Extension, watch_type Type, project *Project, asset *Asset) { // 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.Count; ++i) { if(StringsMatch(TargetPath, WatchHandles.Handles[i].TargetPath)) { Watch = WatchHandles.Handles + i; EraseCurrentStringFromBook(&WatchHandles.Paths); break; } } if(!Watch) { WatchHandles.Handles = FitShrinkable(WatchHandles.Handles, sizeof(*WatchHandles.Handles), WatchHandles.Count, &WatchHandles.Capacity, 8, TRUE); Watch = WatchHandles.Handles + WatchHandles.Count; 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); ++WatchHandles.Count; //PrintWatchHandles(); } PushWatchFileUniquely(Watch, Filename, Extension, Type, Project, Asset); } bool DescriptorIsRedundant(int Descriptor) { bool Result = TRUE; for(int i = 0; i < WatchHandles.Count; ++i) { watch_handle *This = 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.Count; ++i) { watch_handle *This = WatchHandles.Handles + i; if(StringsMatch(Path, This->WatchedPath)) { Result = This->Descriptor; break; } } return Result; } void UpdateWatchHandles(int Descriptor) { for(int i = 0; i < WatchHandles.Count; ++i) { watch_handle *This = 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, sizeof(StoredAsset->Filename)); string FilenameInMemory = Wrap0i(Asset->Filename, sizeof(Asset->Filename)); if(StringsMatch(FilenameInDB, FilenameInMemory) && StoredAsset->Type == Asset->Type) { *Index = i; Result = StoredAsset; break; } Ptr = SkipAsset(StoredAsset); } 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; } int UpdateAssetInDB(asset *Asset) { 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) { StoredAsset->Associated = Asset->Associated; StoredAsset->Variants = Asset->Variants; StoredAsset->Width = Asset->Dimensions.Width; StoredAsset->Height = Asset->Dimensions.Height; if(StoredAsset->Hash != Asset->Hash) { StoredAsset->Hash = Asset->Hash; char *Ptr = (char *)Asset; Ptr += sizeof(*Asset); buffer Checksum = {}; ClaimBuffer(&Checksum, BID_CHECKSUM, 16); CopyStringToBuffer(&Checksum, "%08x", StoredAsset->Hash); file AssetFile = {}; AssetFile.Path = ConstructAssetPath(&AssetFile, Wrap0i(StoredAsset->Filename, sizeof(StoredAsset->Filename)), StoredAsset->Type); ResolvePath(&AssetFile.Path); string ChecksumL = Wrap0i(Checksum.Location, Checksum.Ptr - Checksum.Location); string Message = MakeString("sssslsss", ColourStrings[CS_ONGOING], "Updating", ColourStrings[CS_END], " checksum ", &ChecksumL, " of ", AssetFile.Path, " in HTML files"); fprintf(stderr, "%.*s", (int)Message.Length, Message.Base); uint64_t MessageLength = Message.Length; FreeString(&Message); if(SnipeChecksumIntoHTML(StoredAsset, &Checksum) == RC_SUCCESS) { ClearTerminalRow(MessageLength); fprintf(stderr, "%sUpdated%s checksum %.*s of %s\n", ColourStrings[CS_REINSERTION], ColourStrings[CS_END], (int)ChecksumL.Length, ChecksumL.Base, AssetFile.Path); } DeclaimBuffer(&Checksum); DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); 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 DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); 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; ClearCopyStringNoFormat(StoredAsset.Filename, sizeof(StoredAsset.Filename), Wrap0i(Asset->Filename, sizeof(Asset->Filename))); fwrite(&StoredAsset, sizeof(StoredAsset), 1, DB.Metadata.File.Handle); AccumulateFileEditSize(&DB.Metadata, sizeof(StoredAsset)); printf("%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; } } 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) { Asset->Player = Fit(Asset->Player, sizeof(*Asset->Player), Asset->PlayerLandmarkCount, 8, TRUE); Asset->Player[Asset->PlayerLandmarkCount].Offset = Dest->Ptr - Dest->Location; Asset->Player[Asset->PlayerLandmarkCount].BufferID = Dest->ID; ++Asset->PlayerLandmarkCount; } else { Asset->Search = Fit(Asset->Search, sizeof(*Asset->Search), Asset->SearchLandmarkCount, 8, TRUE); Asset->Search[Asset->SearchLandmarkCount].Offset = Dest->Ptr - Dest->Location; Asset->Search[Asset->SearchLandmarkCount].BufferID = Dest->ID; ++Asset->SearchLandmarkCount; } 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); FreeAndResetCount(A->Player, A->PlayerLandmarkCount); FreeAndResetCount(A->Search, A->SearchLandmarkCount); 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, sizeof(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); return AssetIndexInDB; } //typedef struct //{ // vec2 TileDim; // int32_t XLight; // int32_t XDark; // int32_t YNormal; // int32_t YFocused; // int32_t YDisabled; //} sprite; 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) { ++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); printf("%sNonexistent%s %s asset: %s\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], AssetTypeNames[Type], File.Path); } FreeFile(&File); 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); FreeAndResetCount(Asset->Search, Asset->SearchLandmarkCount); FreeAndResetCount(Asset->Player, Asset->PlayerLandmarkCount); } 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) { printf( " %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) { InitBook(&Assets, sizeof(asset), 16, MBT_ASSET); 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, sizeof(Asset->Filename)), Asset->Type, Asset->Variants, Asset->Associated); Asset = SkipAsset(Asset); } } } void ConstructResolvedAssetURL(buffer *Buffer, asset *Asset, enum8(pages) 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, sizeof(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 ClearNullTerminatedString(char *String) { while(*String) { *String++ = '\0'; } } char * InitialString(char *Dest, string Src) { Free(Dest); string Char = Wrap0i(Src.Base, 1); ExtendString0(&Dest, Char); for(int i = 1; i < Src.Length; ++i) { if(Src.Base[i] == ' ' && i < Src.Length) { ++i; Char = Wrap0i(Src.Base + i, 1); ExtendString0(&Dest, Char); } } return Dest; } char * GetFirstSubstring(char *Dest, string Src) { Free(Dest); string Substring = {}; Substring.Base = Src.Base; for(int i = 0; i < Src.Length; ++i, ++Substring.Length) { if(Src.Base[i] == ' ') { ExtendString0(&Dest, Substring); return Dest; } } ExtendString0(&Dest, Substring); return Dest; } char * InitialAndGetFinalString(char *Dest, string Src) { Free(Dest); int FinalStringBase; for(FinalStringBase = Src.Length; FinalStringBase > 0; --FinalStringBase) { if(Src.Base[FinalStringBase - 1] == ' ') { break; } } if(FinalStringBase > 0 && Src.Base[FinalStringBase] == ' ' && FinalStringBase < Src.Length) { ++FinalStringBase; } string FinalString = Wrap0i(Src.Base + FinalStringBase, Src.Length - FinalStringBase); if(FinalStringBase > 0) { string Initial = Wrap0i(Src.Base, 1); ExtendString0(&Dest, Initial); ExtendString0(&Dest, Wrap0(". ")); for(int i = 0; i < FinalStringBase; ++i) { if(Src.Base[i] == ' ' && i + 1 < FinalStringBase) { ++i; string Initial = Wrap0i(Src.Base + i, 1); ExtendString0(&Dest, Initial); ExtendString0(&Dest, Wrap0(". ")); } } } ExtendString0(&Dest, FinalString); return Dest; } bool AbbreviationsClash(speakers *Speakers) { for(int i = 0; i < Speakers->Count; ++i) { for(int j = i + 1; j < Speakers->Count; ++j) { if(!StringsDiffer0(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[j].Abbreviation)) { return TRUE; } } } return FALSE; } void SortAndAbbreviateSpeakers(speakers *Speakers) { // TODO(matt): Handle Abbreviation in its new form as a char *, rather than a fixed-sized char[], so probably doing // MakeString0() or ExpandString0() or something for(int i = 0; i < Speakers->Count; ++i) { for(int j = i + 1; j < Speakers->Count; ++j) { if(StringsDiffer(Speakers->Speaker[i].Person->ID, Speakers->Speaker[j].Person->ID) > 0) { person *Temp = Speakers->Speaker[j].Person; Speakers->Speaker[j].Person = Speakers->Speaker[i].Person; Speakers->Speaker[i].Person = Temp; break; } } } for(int i = 0; i < Speakers->Count; ++i) { StringToColourHash(&Speakers->Speaker[i].Colour, Speakers->Speaker[i].Person->ID); Speakers->Speaker[i].Abbreviation = InitialString(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Person->Name); } int Attempt = 0; while(AbbreviationsClash(Speakers)) { for(int i = 0; i < Speakers->Count; ++i) { switch(Attempt) { case 0: Speakers->Speaker[i].Abbreviation = GetFirstSubstring(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Person->Name); break; case 1: Speakers->Speaker[i].Abbreviation = InitialAndGetFinalString(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Person->Name); break; case 2: Free(Speakers->Speaker[i].Abbreviation); ExtendString0(&Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Person->Name); break; } } ++Attempt; } } 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, sizeof(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 SkipProjectAndChildren(Project); } 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 = SkipAsset(This); } 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, sizeof(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(); } char *RoleStrings[] = { "Cohost", "Guest", "Host", "Indexer", }; typedef enum { R_COHOST, R_GUEST, R_HOST, R_INDEXER, } role; 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); } } } void PushCredentials(buffer *CreditsMenu, speakers *Speakers, person *Actor, role Role, bool *RequiresCineraJS) { if(Role != R_INDEXER) { Speakers->Speaker = Fit(Speakers->Speaker, sizeof(*Speakers->Speaker), Speakers->Count, 4, TRUE); Speakers->Speaker[Speakers->Count].Person = Actor; ++Speakers->Count; } if(CreditsMenu->Ptr == CreditsMenu->Location) { CopyStringToBuffer(CreditsMenu, "
\n" " Credits\n" "
\n"); } CopyStringToBuffer(CreditsMenu, " \n"); if(Actor->Homepage.Length) { CopyStringToBuffer(CreditsMenu, " \n" "
%s
\n" "
%.*s
\n" "
\n", (int)Actor->Homepage.Length, Actor->Homepage.Base, RoleStrings[Role], (int)Actor->Name.Length, Actor->Name.Base); } else { CopyStringToBuffer(CreditsMenu, "
\n" "
%s
\n" "
%.*s
\n" "
\n", RoleStrings[Role], (int)Actor->Name.Length, Actor->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 FreeCredentials(speakers *S) { FreeAndResetCount(S->Speaker, S->Count); } void WaitForInput() { #if 0 fprintf(stderr, "Press Enter to continue...\n"); getchar(); #endif } void ErrorCredentials(string Actor, role Role) { Colourise(CS_ERROR); fprintf(stderr, "No credentials for %s %.*s\n", RoleStrings[Role], (int)Actor.Length, Actor.Base); Colourise(CS_END); fprintf(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(); } int BuildCredits(buffer *CreditsMenu, HMML_VideoMetaData *Metadata, speakers *Speakers, bool *RequiresCineraJS) { person *Host = GetPersonFromConfig(Wrap0(Metadata->member)); if(Host) { PushCredentials(CreditsMenu, Speakers, Host, R_HOST, RequiresCineraJS); } else { ErrorCredentials(Wrap0(Metadata->member), R_HOST); return CreditsError_NoCredentials; } for(int i = 0; i < Metadata->co_host_count; ++i) { person *CoHost = GetPersonFromConfig(Wrap0(Metadata->co_hosts[i])); if(CoHost) { PushCredentials(CreditsMenu, Speakers, CoHost, R_COHOST, RequiresCineraJS); } else { ErrorCredentials(Wrap0(Metadata->co_hosts[i]), R_COHOST); return CreditsError_NoCredentials; } } for(int i = 0; i < Metadata->guest_count; ++i) { person *Guest = GetPersonFromConfig(Wrap0(Metadata->guests[i])); if(Guest) { PushCredentials(CreditsMenu, Speakers, Guest, R_GUEST, RequiresCineraJS); } else { ErrorCredentials(Wrap0(Metadata->guests[i]), R_GUEST); return CreditsError_NoCredentials; } } if(Speakers->Count > 1) { SortAndAbbreviateSpeakers(Speakers); } if(Metadata->annotator_count > 0) { for(int i = 0; i < Metadata->annotator_count; ++i) { person *Indexer = GetPersonFromConfig(Wrap0(Metadata->annotators[i])); if(Indexer) { PushCredentials(CreditsMenu, Speakers, Indexer, R_INDEXER, RequiresCineraJS); } else { ErrorCredentials(Wrap0(Metadata->annotators[i]), R_INDEXER); return CreditsError_NoCredentials; } } } else { if(CreditsMenu->Ptr > CreditsMenu->Location) { CopyStringToBuffer(CreditsMenu, "
\n" "
\n"); } fprintf(stderr, "Missing \"indexer\" in the [video] node\n"); return CreditsError_NoIndexer; } if(CreditsMenu->Ptr > CreditsMenu->Location) { CopyStringToBuffer(CreditsMenu, " \n" " \n"); } return RC_SUCCESS; } enum { REF_SITE = 1 << 0, REF_PAGE = 1 << 1, REF_URL = 1 << 2, REF_TITLE = 1 << 3, REF_ARTICLE = 1 << 4, REF_AUTHOR = 1 << 5, REF_EDITOR = 1 << 6, REF_PUBLISHER = 1 << 7, REF_ISBN = 1 << 8, } reference_fields; int BuildReference(ref_info *ReferencesArray, int RefIdentifier, int UniqueRefs, HMML_Reference *Ref, HMML_Annotation *Anno) { if(Ref->isbn) { CopyString(ReferencesArray[UniqueRefs].ID, sizeof(ReferencesArray[UniqueRefs].ID), "%s", Ref->isbn); if(!Ref->url) { CopyString(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), "https://isbndb.com/book/%s", Ref->isbn); } else { CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), Wrap0(Ref->url)); } } else if(Ref->url) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, sizeof(ReferencesArray[UniqueRefs].ID), Wrap0(Ref->url)); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), Wrap0(Ref->url)); } else { return RC_INVALID_REFERENCE; } int Mask = 0; if(Ref->site) { Mask |= REF_SITE; } if(Ref->page) { Mask |= REF_PAGE; } if(Ref->title) { Mask |= REF_TITLE; } if(Ref->article) { Mask |= REF_ARTICLE; } if(Ref->author) { Mask |= REF_AUTHOR; } if(Ref->editor) { Mask |= REF_EDITOR; } if(Ref->publisher) { Mask |= REF_PUBLISHER; } // TODO(matt): Consider handling the various combinations more flexibly, unless we defer this stuff until we have the // reference store, in which we could optionally customise the display of each reference entry switch(Mask) { case (REF_TITLE | REF_AUTHOR | REF_PUBLISHER): { CopyString(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), "%s (%s)", Ref->author, Ref->publisher); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->title)); } break; case (REF_AUTHOR | REF_SITE | REF_PAGE): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->site)); CopyString(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), "%s: \"%s\"", Ref->author, Ref->page); } break; case (REF_PAGE | REF_TITLE): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->title)); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->page)); } break; case (REF_SITE | REF_PAGE): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->site)); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->page)); } break; case (REF_SITE | REF_TITLE): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->site)); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->title)); } break; case (REF_TITLE | REF_AUTHOR): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->author)); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->title)); } break; case (REF_ARTICLE | REF_AUTHOR): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->author)); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->article)); } break; case (REF_TITLE | REF_PUBLISHER): { CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->publisher)); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->title)); } break; case REF_TITLE: { CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->title)); } break; case REF_SITE: { CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->site)); } break; default: return RC_INVALID_REFERENCE; break; } CopyString(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode, sizeof(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode), "%s", Anno->time); ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Identifier = RefIdentifier; return RC_SUCCESS; } void InsertCategory(categories *GlobalTopics, categories *LocalTopics, categories *GlobalMedia, categories *LocalMedia, string Marker) { medium *Medium = GetMediumFromProject(CurrentProject, Marker); if(Medium) { int MediumIndex; for(MediumIndex = 0; MediumIndex < LocalMedia->Count; ++MediumIndex) { if(!StringsDifferLv0(Medium->ID, LocalMedia->Category[MediumIndex].Marker)) { return; } if((StringsDifferLv0(Medium->Name, LocalMedia->Category[MediumIndex].WrittenText)) < 0) { int CategoryCount; for(CategoryCount = LocalMedia->Count; CategoryCount > MediumIndex; --CategoryCount) { ClearCopyString(LocalMedia->Category[CategoryCount].Marker, sizeof(LocalMedia->Category[CategoryCount].Marker), "%s", LocalMedia->Category[CategoryCount-1].Marker); ClearCopyString(LocalMedia->Category[CategoryCount].WrittenText, sizeof(LocalMedia->Category[CategoryCount].WrittenText), "%s", LocalMedia->Category[CategoryCount-1].WrittenText); } ClearCopyString(LocalMedia->Category[CategoryCount].Marker, sizeof(LocalMedia->Category[CategoryCount].Marker), "%.*s", (int)Medium->ID.Length, Medium->ID.Base); ClearCopyString(LocalMedia->Category[CategoryCount].WrittenText, sizeof(LocalMedia->Category[CategoryCount].WrittenText), "%.*s", (int)Medium->Name.Length, Medium->Name.Base); break; } } if(MediumIndex == LocalMedia->Count) { CopyString(LocalMedia->Category[MediumIndex].Marker, sizeof(LocalMedia->Category[MediumIndex].Marker), "%.*s", (int)Medium->ID.Length, Medium->ID.Base); CopyString(LocalMedia->Category[MediumIndex].WrittenText, sizeof(LocalMedia->Category[MediumIndex].WrittenText), "%.*s", (int)Medium->Name.Length, Medium->Name.Base); } ++LocalMedia->Count; for(MediumIndex = 0; MediumIndex < GlobalMedia->Count; ++MediumIndex) { if(!StringsDifferLv0(Medium->ID, GlobalMedia->Category[MediumIndex].Marker)) { return; } if((StringsDifferLv0(Medium->Name, GlobalMedia->Category[MediumIndex].WrittenText)) < 0) { int CategoryCount; for(CategoryCount = GlobalMedia->Count; CategoryCount > MediumIndex; --CategoryCount) { ClearCopyString(GlobalMedia->Category[CategoryCount].Marker, sizeof(GlobalMedia->Category[CategoryCount].Marker), "%s", GlobalMedia->Category[CategoryCount-1].Marker); ClearCopyString(GlobalMedia->Category[CategoryCount].WrittenText, sizeof(GlobalMedia->Category[CategoryCount].WrittenText), "%s", GlobalMedia->Category[CategoryCount-1].WrittenText); } ClearCopyString(GlobalMedia->Category[CategoryCount].Marker, sizeof(GlobalMedia->Category[CategoryCount].Marker), "%.*s", (int)Medium->ID.Length, Medium->ID.Base); ClearCopyString(GlobalMedia->Category[CategoryCount].WrittenText, sizeof(GlobalMedia->Category[CategoryCount].WrittenText), "%.*s", (int)Medium->Name.Length, Medium->Name.Base); break; } } if(MediumIndex == GlobalMedia->Count) { CopyString(GlobalMedia->Category[MediumIndex].Marker, sizeof(GlobalMedia->Category[MediumIndex].Marker), "%.*s", (int)Medium->ID.Length, Medium->ID.Base); CopyString(GlobalMedia->Category[MediumIndex].WrittenText, sizeof(GlobalMedia->Category[MediumIndex].WrittenText), "%.*s", (int)Medium->Name.Length, Medium->Name.Base); } ++GlobalMedia->Count; } else { int TopicIndex; for(TopicIndex = 0; TopicIndex < LocalTopics->Count; ++TopicIndex) { if(!StringsDifferLv0(Marker, LocalTopics->Category[TopicIndex].Marker)) { return; } if((StringsDifferLv0(Marker, LocalTopics->Category[TopicIndex].Marker)) < 0) { int CategoryCount; for(CategoryCount = LocalTopics->Count; CategoryCount > TopicIndex; --CategoryCount) { ClearCopyString(LocalTopics->Category[CategoryCount].Marker, sizeof(LocalTopics->Category[CategoryCount].Marker), "%s", LocalTopics->Category[CategoryCount-1].Marker); } ClearCopyString(LocalTopics->Category[CategoryCount].Marker, sizeof(LocalTopics->Category[CategoryCount].Marker), "%.*s", (int)Marker.Length, Marker.Base); break; } } if(TopicIndex == LocalTopics->Count) { CopyString(LocalTopics->Category[TopicIndex].Marker, sizeof(LocalTopics->Category[TopicIndex].Marker), "%.*s", (int)Marker.Length, Marker.Base); } ++LocalTopics->Count; for(TopicIndex = 0; TopicIndex < GlobalTopics->Count; ++TopicIndex) { if(!StringsDifferLv0(Marker, GlobalTopics->Category[TopicIndex].Marker)) { return; } // NOTE(matt): This successfully sorts "nullTopic" at the end, but maybe figure out a more general way to force the // order of stuff, perhaps blocks of dudes that should sort to the start / end if(((StringsDifferLv0(Marker, GlobalTopics->Category[TopicIndex].Marker)) < 0 || !StringsDiffer0(GlobalTopics->Category[TopicIndex].Marker, "nullTopic"))) { if(StringsDifferLv0(Marker, "nullTopic")) // NOTE(matt): This test (with the above || condition) forces nullTopic never to be inserted, only appended { int CategoryCount; for(CategoryCount = GlobalTopics->Count; CategoryCount > TopicIndex; --CategoryCount) { ClearCopyString(GlobalTopics->Category[CategoryCount].Marker, sizeof(GlobalTopics->Category[CategoryCount].Marker), "%s", GlobalTopics->Category[CategoryCount-1].Marker); } ClearCopyString(GlobalTopics->Category[CategoryCount].Marker, sizeof(GlobalTopics->Category[CategoryCount].Marker), "%.*s", (int)Marker.Length, Marker.Base); break; } } } if(TopicIndex == GlobalTopics->Count) { CopyString(GlobalTopics->Category[TopicIndex].Marker, sizeof(GlobalTopics->Category[TopicIndex].Marker), "%.*s", (int)Marker.Length, Marker.Base); } ++GlobalTopics->Count; } } void BuildTimestampClass(buffer *TimestampClass, categories *LocalTopics, categories *LocalMedia, string DefaultMedium) { if(LocalTopics->Count == 1 && !StringsDiffer0(LocalTopics->Category[0].Marker, "nullTopic")) { // NOTE(matt): Stack-string char SanitisedMarker[StringLength(LocalTopics->Category[0].Marker) + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalTopics->Category[0].Marker); SanitisePunctuation(SanitisedMarker); CopyStringToBuffer(TimestampClass, " cat_%s", SanitisedMarker); } else { for(int i = 0; i < LocalTopics->Count; ++i) { // NOTE(matt): Stack-string char SanitisedMarker[StringLength(LocalTopics->Category[i].Marker) + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalTopics->Category[i].Marker); SanitisePunctuation(SanitisedMarker); CopyStringToBuffer(TimestampClass, " cat_%s", SanitisedMarker); } } if(LocalMedia->Count == 1 && !StringsDifferLv0(DefaultMedium, LocalMedia->Category[0].Marker)) { // NOTE(matt): Stack-string char SanitisedMarker[StringLength(LocalMedia->Category[0].Marker) + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalMedia->Category[0].Marker); SanitisePunctuation(SanitisedMarker); CopyStringToBuffer(TimestampClass, " %s", SanitisedMarker); } else { for(int i = 0; i < LocalMedia->Count; ++i) { // NOTE(matt): Stack-string char SanitisedMarker[StringLength(LocalMedia->Category[i].Marker) + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalMedia->Category[i].Marker); SanitisePunctuation(SanitisedMarker); medium *Medium = GetMediumFromProject(CurrentProject, Wrap0i(LocalMedia->Category[i].Marker, sizeof(LocalMedia->Category[i].Marker))); if(Medium) { if(Medium->Hidden) { CopyStringToBuffer(TimestampClass, " off_%s skip", SanitisedMarker); } else { CopyStringToBuffer(TimestampClass, " %s", SanitisedMarker); } } } } CopyStringToBuffer(TimestampClass, "\""); } void BuildCategoryIcons(buffer *CategoryIcons, categories *LocalTopics, categories *LocalMedia, string DefaultMedium, bool *RequiresCineraJS) { bool CategoriesSpan = FALSE; if(!(LocalTopics->Count == 1 && !StringsDiffer0(LocalTopics->Category[0].Marker, "nullTopic") && LocalMedia->Count == 1 && !StringsDifferLv0(DefaultMedium, LocalMedia->Category[0].Marker))) { CategoriesSpan = TRUE; CopyStringToBuffer(CategoryIcons, ""); } if(!(LocalTopics->Count == 1 && !StringsDiffer0(LocalTopics->Category[0].Marker, "nullTopic"))) { for(int i = 0; i < LocalTopics->Count; ++i) { // NOTE(matt): Stack-string char SanitisedMarker[StringLength(LocalTopics->Category[i].Marker) + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalTopics->Category[i].Marker); SanitisePunctuation(SanitisedMarker); CopyStringToBuffer(CategoryIcons, "
", LocalTopics->Category[i].Marker, SanitisedMarker); } } if(!(LocalMedia->Count == 1 && !StringsDifferLv0(DefaultMedium, LocalMedia->Category[0].Marker))) { for(int i = 0; i < LocalMedia->Count; ++i) { // NOTE(matt): Stack-string char SanitisedMarker[StringLength(LocalMedia->Category[i].Marker) + 1]; CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalMedia->Category[i].Marker); SanitisePunctuation(SanitisedMarker); medium *Medium = GetMediumFromProject(CurrentProject, Wrap0i(LocalMedia->Category[i].Marker, sizeof(LocalMedia->Category[i].Marker))); if(Medium && !Medium->Hidden) { CopyStringToBuffer(CategoryIcons, "
", LocalMedia->Category[i].WrittenText, LocalMedia->Category[i].Marker); 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) { fprintf(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); if((Result = curl_easy_perform(curl))) { fprintf(stderr, "%s\n", curl_easy_strerror(Result)); } curl_easy_cleanup(curl); } 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; } rc SearchQuotes(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 ',' long int Time = StringToInt(InTime); // NOTE(matt): Stack-string char DayString[3] = { 0 }; strftime(DayString, 3, "%d", gmtime(&Time)); int Day = String0ToInt(DayString); // NOTE(matt): Stack-string char DaySuffix[3]; if(DayString[1] == '1' && Day != 11) { CopyString(DaySuffix, sizeof(DaySuffix), "st"); } else if(DayString[1] == '2' && Day != 12) { CopyString(DaySuffix, sizeof(DaySuffix), "nd"); } else if(DayString[1] == '3' && Day != 13) { CopyString(DaySuffix, sizeof(DaySuffix), "rd"); } else { CopyString(DaySuffix, sizeof(DaySuffix), "th"); } // NOTE(matt): Stack-string char MonthYear[32]; strftime(MonthYear, 32, "%B, %Y", gmtime(&Time)); CopyString(Info->Date, sizeof(Info->Date), "%d%s %s", Day, DaySuffix, MonthYear); CopyStringNoFormatT(Info->Text, sizeof(Info->Text), QuoteStaging->Ptr, '\n'); Result = RC_FOUND; break; } else { while(*QuoteStaging->Ptr != '\n') { ++QuoteStaging->Ptr; } ++QuoteStaging->Ptr; } } return Result; } rc BuildQuote(quote_info *Info, string Speaker, int ID, bool ShouldFetchQuotes) { rc Result = RC_SUCCESS; // 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); FILE *QuoteCache; // 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; if(!(QuoteCache = fopen(QuoteCachePath, "a+"))) { if(MakeDir(Wrap0i(QuoteCacheDir, sizeof(QuoteCacheDir)))) { CacheAvailable = TRUE; } if(!(QuoteCache = fopen(QuoteCachePath, "a+"))) { fprintf(stderr, "Unable to open quote cache %s: %s\n", QuoteCachePath, strerror(errno)); } else { CacheAvailable = TRUE; } } else { CacheAvailable = TRUE; } buffer QuoteStaging = {}; QuoteStaging.ID = BID_QUOTE_STAGING; QuoteStaging.Size = Kilobytes(256); if(!(QuoteStaging.Location = malloc(QuoteStaging.Size))) { fclose(QuoteCache); Result = RC_ERROR_MEMORY; } if(Result != RC_ERROR_MEMORY) { #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Allocated QuoteStaging (%ld)\n", QuoteStaging.Size); fclose(MemLog); printf(" Allocated QuoteStaging (%ld)\n", QuoteStaging.Size); #endif QuoteStaging.Ptr = QuoteStaging.Location; if(CacheAvailable) { fseek(QuoteCache, 0, SEEK_END); int FileSize = ftell(QuoteCache); fseek(QuoteCache, 0, SEEK_SET); fread(QuoteStaging.Location, FileSize, 1, QuoteCache); fclose(QuoteCache); if(ShouldFetchQuotes || SearchQuotes(&QuoteStaging, FileSize, Info, ID) == RC_UNFOUND) { if(CurlQuotes(&QuoteStaging, QuotesURL) == CURLE_OK) { if(!(QuoteCache = fopen(QuoteCachePath, "w"))) { perror(QuoteCachePath); } fwrite(QuoteStaging.Location, QuoteStaging.Ptr - QuoteStaging.Location, 1, QuoteCache); fclose(QuoteCache); int CacheSize = QuoteStaging.Ptr - QuoteStaging.Location; QuoteStaging.Ptr = QuoteStaging.Location; Result = SearchQuotes(&QuoteStaging, CacheSize, Info, ID); } else { Result = RC_UNFOUND; } } } else { if(CurlQuotes(&QuoteStaging, QuotesURL) == CURLE_OK) { int CacheSize = QuoteStaging.Ptr - QuoteStaging.Location; QuoteStaging.Ptr = QuoteStaging.Location; Result = SearchQuotes(&QuoteStaging, CacheSize, Info, ID); } else { Result = RC_UNFOUND; } } FreeBuffer(&QuoteStaging); } return Result; } int GenerateTopicColours(neighbourhood *N, string Topic) { // 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) { return RC_NOOP; } file Topics = {}; 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; // TODO(matt): open() if(!(CSSDirHandle = opendir(Topics.Path))) { if(!MakeDir(Wrap0(Topics.Path))) { LogError(LOG_ERROR, "Unable to create directory %s: %s", Topics.Path, strerror(errno)); fprintf(stderr, "Unable to create directory %s: %s\n", Topics.Path, strerror(errno)); return RC_ERROR_DIRECTORY; }; } closedir(CSSDirHandle); *Ptr = '/'; if((Topics.Handle = fopen(Topics.Path, "a+"))) { fseek(Topics.Handle, 0, SEEK_END); Topics.Buffer.Size = ftell(Topics.Handle); fseek(Topics.Handle, 0, SEEK_SET); if(!(Topics.Buffer.Location = malloc(Topics.Buffer.Size))) { return RC_ERROR_MEMORY; } #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Allocated Topics (%ld)\n", Topics.Buffer.Size); fclose(MemLog); printf(" Allocated Topics (%ld)\n", Topics.Buffer.Size); #endif Topics.Buffer.Ptr = Topics.Buffer.Location; fread(Topics.Buffer.Location, Topics.Buffer.Size, 1, Topics.Handle); while(Topics.Buffer.Ptr - Topics.Buffer.Location < Topics.Buffer.Size) { Topics.Buffer.Ptr += StringLength(".category."); if(!StringsDifferT(SanitisedTopic, Topics.Buffer.Ptr, ' ')) { FreeBuffer(&Topics.Buffer); fclose(Topics.Handle); return RC_NOOP; } while(Topics.Buffer.Ptr - Topics.Buffer.Location < Topics.Buffer.Size && *Topics.Buffer.Ptr != '\n') { ++Topics.Buffer.Ptr; } ++Topics.Buffer.Ptr; } if(!StringsDifferLv0(Topic, "nullTopic")) { fprintf(Topics.Handle, ".category.%s { border: 1px solid transparent; background: transparent; }\n", SanitisedTopic); } else { hsl_colour Colour; StringToColourHash(&Colour, Topic); fprintf(Topics.Handle, ".category.%s { border: 1px solid hsl(%d, %d%%, %d%%); background: hsl(%d, %d%%, %d%%); }\n", SanitisedTopic, Colour.Hue, Colour.Saturation, Colour.Lightness, Colour.Hue, Colour.Saturation, Colour.Lightness); } fclose(Topics.Handle); #if DEBUG_MEM MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Freed Topics (%ld)\n", Topics.Buffer.Size); fclose(MemLog); printf(" Freed Topics (%ld)\n", Topics.Buffer.Size); #endif FreeBuffer(&Topics.Buffer); 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); } return RC_SUCCESS; } else { // NOTE(matt): Maybe it shouldn't be possible to hit this case now that we MakeDir the actual dir... perror(Topics.Path); return RC_ERROR_FILE; } } /* * NOTE(matt); * * Documentation structure: * * section * * option * description */ void ResetConfigIdentifierDescriptionDisplayedBools(void) { for(int i = 0; i < IDENT_COUNT; ++i) { ConfigIdentifiers[i].IdentifierDescriptionDisplayed = FALSE; ConfigIdentifiers[i].IdentifierDescription_MediumDisplayed = FALSE; ConfigIdentifiers[i].LocalVariableDescriptionDisplayed = FALSE; } } void PrintHelpConfig(void) { ResetConfigIdentifierDescriptionDisplayedBools(); // Config Syntax int IndentationLevel = 0; NewSection("Configuration", &IndentationLevel); NewSection("Assigning values", &IndentationLevel); PrintC(CS_YELLOW_BOLD, "identifier"); fprintf(stderr, " = "); PrintC(CS_GREEN_BOLD, "\"string\""); fprintf(stderr, ";"); IndentedCarriageReturn(IndentationLevel); PrintC(CS_YELLOW_BOLD, "identifier"); fprintf(stderr, " = "); PrintC(CS_BLUE_BOLD, "number"); fprintf(stderr, ";"); IndentedCarriageReturn(IndentationLevel); PrintC(CS_YELLOW_BOLD, "identifier"); fprintf(stderr, " = "); PrintC(CS_GREEN_BOLD, "\"boolean\""); fprintf(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"); fprintf(stderr, " = "); PrintC(CS_GREEN_BOLD, "\"scope\""); fprintf(stderr, " {"); IndentedCarriageReturn(IndentationLevel); fprintf(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.")); config_type_specs TypeSpecs = InitTypeSpecs(); PrintTypeSpecs(&TypeSpecs, IndentationLevel); //FreeTypeSpecs(&TypeSpecs); EndSection(&IndentationLevel); fprintf(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); fprintf(stderr, "\n"); NewSection("Environment variables", &IndentationLevel); TypesetString(INDENT_WIDTH *IndentationLevel, Wrap0("Run `export` to see all available environment variables")); EndSection(&IndentationLevel); EndSection(&IndentationLevel); // Variables fprintf(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); fprintf(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 = calloc(1, sizeof(scope_tree)); SetTypeSpec(ScopeTree, &TypeSpecs); SetDefaults(ScopeTree, &TypeSpecs); PrintScopeTree(ScopeTree, IndentationLevel); FreeScopeTree(ScopeTree); FreeTypeSpecs(&TypeSpecs); #endif fprintf(stderr, "\n"); } void PrintHelp(char *BinaryLocation, char *DefaultConfigPath) { // Options fprintf(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_(char *BinaryLocation) { #if AFE fprintf(stderr, "Usage: %s [option(s)] filename(s)\n" "\n" "Options:\n" " Paths: %s(advisedly universal, but may be set per-(sub)project as required)%s\n" " -r \n" " Override default assets root directory (\"%s\")\n" " -R \n" " Override default assets root URL (\"%s\")\n" " %sIMPORTANT%s: -r and -R must correspond to the same location\n" " %sUNSUPPORTED: If you move files from RootDir, the RootURL should\n" " correspond to the resulting location%s\n" "\n" " -c \n" " Override default CSS directory (\"%s\"), relative to root\n" " -i \n" " Override default images directory (\"%s\"), relative to root\n" " -j \n" " Override default JS directory (\"%s\"), relative to root\n" " -Q \n" " Override default query string (\"%s\")\n" " To disable revved resources, set an empty string with -Q \"\"\n" "\n" " Project Settings:\n" " -p \n" " Set the project ID, triggering PROJECT EDITION\n" " -m \n" " Override default default medium (\"%s\")\n" " %sKnown project defaults:\n", BinaryLocation, ColourStrings[CS_COMMENT], ColourStrings[CS_END], DefaultConfig->RootDir, DefaultConfig->RootURL, ColourStrings[CS_ERROR], ColourStrings[CS_END], ColourStrings[CS_COMMENT], ColourStrings[CS_END], DefaultConfig->CSSDir, DefaultConfig->ImagesDir, DefaultConfig->JSDir, DefaultConfig->QueryString, DefaultConfig->DefaultMedium, ColourStrings[CS_COMMENT]); for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex) { fprintf(stderr, " %s:", ProjectInfo[ProjectIndex].ProjectID); // NOTE(matt): This kind of thing really needs to loop over the dudes once to find the longest one for(int i = 11; i > StringLength(ProjectInfo[ProjectIndex].ProjectID); i -= 4) { fprintf(stderr, "\t"); } fprintf(stderr, "%s\n", ProjectInfo[ProjectIndex].Medium); } fprintf(stderr, "%s -s