#if 0 ctime -begin ${0%.*}.ctm gcc -g -fsanitize=address -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl #gcc -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl ctime -end ${0%.*}.ctm exit #endif typedef struct { unsigned int Major, Minor, Patch; } version; version CINERA_APP_VERSION = { .Major = 0, .Minor = 5, .Patch = 18 }; // TODO(matt): Copy in the DB 3 stuff from cinera_working.c #define CINERA_DB_VERSION 2 #define DEBUG 0 #define DEBUG_MEM 0 typedef unsigned int bool; #define TRUE 1 #define FALSE 0 #include // NOTE(matt): varargs #include // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror #include // NOTE(matt): calloc, malloc, free #include "hmmlib.h" #include // NOTE(matt): getopts //#include "config.h" // TODO(matt): Implement config.h #include #include #include #include #include #include // NOTE(matt): strerror #include //NOTE(matt): errno #include // NOTE(matt): inotify #include // NOTE(matt): sleep() #define Kilobytes(Bytes) Bytes << 10 #define Megabytes(Bytes) Bytes << 20 enum { EDITION_SINGLE, EDITION_PROJECT, EDITION_NETWORK } editions; enum { // NOTE(matt): https://tools.ietf.org/html/rfc5424#section-6.2.1 LOG_EMERGENCY, LOG_ALERT, LOG_CRITICAL, LOG_ERROR, LOG_WARNING, LOG_NOTICE, LOG_INFORMATIONAL, LOG_DEBUG } log_levels; enum { MODE_ONESHOT = 1 << 0, } modes; enum { RC_ARENA_FULL, RC_ERROR_DIRECTORY, RC_ERROR_FATAL, RC_ERROR_FILE, RC_ERROR_HMML, RC_ERROR_MAX_REFS, RC_ERROR_MEMORY, RC_ERROR_PROJECT, RC_ERROR_QUOTE, RC_ERROR_SEEK, RC_FOUND, RC_UNFOUND, RC_INVALID_REFERENCE, RC_INVALID_TEMPLATE, RC_NOOP, RC_RIP, RC_SUCCESS } returns; typedef struct { void *Location; void *Ptr; char *ID; int Size; } arena; typedef struct { char *Location; char *Ptr; char *ID; int Size; } buffer; typedef struct { buffer Buffer; FILE *Handle; char Path[256]; int FileSize; } file_buffer; typedef struct { buffer IncludesIndex; buffer Search; buffer Index; // NOTE(matt): This buffer is malloc'd separately, rather than claimed from the memory_arena buffer ScriptIndex; buffer IncludesPlayer; buffer Menus; buffer Player; buffer ScriptPlayer; char ProjectName[32]; char Title[256]; char URLIndex[2048]; char URLPlayer[2048]; char VideoID[16]; } buffers; enum { // Contents Page TAG_INDEX, // Player Page TAG_INCLUDES, TAG_MENUS, TAG_PLAYER, TAG_SCRIPT, // Anywhere TAG_PROJECT, TAG_TITLE, TAG_URL, TAG_VIDEO_ID, } template_tags; typedef struct { int Code; // template_tags char *Tag; } tag; tag Tags[] = { { TAG_INDEX, "__CINERA_INDEX__" }, { TAG_INCLUDES, "__CINERA_INCLUDES__" }, { TAG_MENUS, "__CINERA_MENUS__" }, { TAG_PLAYER, "__CINERA_PLAYER__" }, { TAG_SCRIPT, "__CINERA_SCRIPT__" }, { TAG_PROJECT, "__CINERA_PROJECT__" }, { TAG_TITLE, "__CINERA_TITLE__" }, { TAG_URL, "__CINERA_URL__" }, { TAG_VIDEO_ID, "__CINERA_VIDEO_ID__" }, }; typedef struct { int Offset; int TagCode; } tag_offset; typedef struct { char Filename[256]; tag_offset Tag[16]; int Validity; // NOTE(matt): Bitmask describing which page the template is valid for, i.e. contents and / or player page int TagCount; } template_metadata; typedef struct { template_metadata Metadata; buffer Buffer; } template; typedef struct { // Universal char CacheDir[256]; int Edition; int LogLevel; int Mode; int UpdateInterval; bool ForceIntegration; // Advisedly universal, although could be per-project char *RootDir; // Absolute char *RootURL; char *CSSDir; // Relative to Root{Dir,URL} char *ImagesDir; // Relative to Root{Dir,URL} char *JSDir; // Relative to Root{Dir,URL} // Per Project char *ProjectID; char *Theme; char *DefaultMedium; // Per Project - Input char *ProjectDir; // Absolute char *TemplatesDir; // Absolute char *TemplateIndexLocation; // Relative to TemplatesDir ??? char *TemplatePlayerLocation; // Relative to TemplatesDir ??? // Per Project - Output char *BaseDir; // Absolute char *BaseURL; char *IndexLocation; // Relative to Base{Dir,URL} char *PlayerLocation; // Relative to Base{Dir,URL} char *PlayerURLPrefix; /* NOTE(matt): This will become a full blown customisable output URL. For now it simply replaces the ProjectID */ // Single Edition - Input char SingleHMMLFilePath[256]; // Single Edition - Output char *OutLocation; char *OutIntegratedLocation; } config; // NOTE(matt): Globals config Config = {}; arena MemoryArena; // 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 REF_MAX_IDENTIFIER 64 typedef struct { char RefTitle[620]; char ID[512]; char URL[512]; char Source[256]; identifier Identifier[REF_MAX_IDENTIFIER]; int IdentifierCount; } ref_info; typedef struct { char Marker[32]; char WrittenText[32]; } category_info; typedef struct { category_info Category[64]; int Count; } categories; // TODO(matt): Parse this stuff out of a config file typedef struct { char *Username; char *CreditedName; char *HomepageURL; char *SupportIcon; char *SupportURL; } credential_info; credential_info Credentials[] = { { "/a_waterman", "Andrew Waterman", "https://www.linkedin.com/in/andrew-waterman-76805788", "", ""}, { "/y_lee", "Yunsup Lee", "https://www.linkedin.com/in/yunsup-lee-385b692b/", "", ""}, { "AndrewJDR", "Andrew Johnson", "", "", ""}, { "AsafGartner", "Asaf Gartner", "", "", ""}, { "BretHudson", "Bret Hudson", "http://www.brethudson.com/", "cinera_sprite_patreon.png", "https://www.patreon.com/indieFunction"}, { "ChronalDragon", "Andrew Chronister", "http://chronal.net/", "", ""}, { "Kelimion", "Jeroen van Rijn", "https://handmade.network/home", "", ""}, { "Mannilie", "Emmanuel Vaccaro", "http://emmanuelvaccaro.com/", "", ""}, { "Miblo", "Matt Mascarenhas", "http://miblodelcarpio.co.uk", "", ""}, { "Mr4thDimention", "Allen Webster", "http://www.4coder.net/", "", ""}, { "Quel_Solaar", "Eskil Steenberg", "http://quelsolaar.com/", "", ""}, { "ZedZull", "Jay Waggle", "", "", ""}, { "abnercoimbre", "Abner Coimbre", "https://handmade.network/m/abnercoimbre", "", ""}, { "brianwill", "Brian Will", "http://brianwill.net/blog/", "", ""}, { "cbloom", "Charles Bloom", "http://cbloomrants.blogspot.co.uk/", "", ""}, { "cmuratori", "Casey Muratori", "https://handmadehero.org", "cinera_sprite_sendowl.png", "https://handmadehero.org/patreon.html"}, { "csnover", "Colin Snover", "https://zetafleet.com/", "", ""}, { "debiatan", "Miguel Lechón", "http://blog.debiatan.net/", "", ""}, { "dspecht", "Dustin Specht", "", "", ""}, { "effect0r", "Cory Henderlite", "", "", ""}, { "ffsjs", "ffsjs", "", "", ""}, { "fierydrake", "Mike Tunnicliffe", "", "", ""}, { "garlandobloom", "Matthew VanDevander", "https://lowtideproductions.com/", "cinera_sprite_patreon.png", "https://www.patreon.com/mv"}, { "ikerms", "Iker Murga", "", "", ""}, { "insofaras", "Alex Baines", "https://abaines.me.uk/", "", ""}, { "jacebennett", "Jace Bennett", "", "", ""}, { "jon", "Jonathan Blow", "http://the-witness.net/news/", "", ""}, { "jpike", "Jacob Pike", "", "", ""}, { "martincohen", "Martin Cohen", "http://blog.coh.io/", "", ""}, { "miotatsu", "Mio Iwakura", "http://riscy.tv/", "cinera_sprite_patreon.png", "https://patreon.com/miotatsu"}, { "nothings", "Sean Barrett", "https://nothings.org/", "", ""}, { "philipbuuck", "Philip Buuck", "http://philipbuuck.com/", "", ""}, { "powerc9000", "Clay Murray", "http://claymurray.website/", "", ""}, { "rygorous", "Fabian Giesen", "https://fgiesen.wordpress.com/", "", ""}, { "schme", "Kasper Sauramo", "", "", ""}, { "sssmcgrath", "Shawn McGrath", "http://www.dyadgame.com/", "", ""}, { "thehappiecat", "Anne", "https://www.youtube.com/c/TheHappieCat", "cinera_sprite_patreon.png", "https://www.patreon.com/thehappiecat"}, { "theinternetftw", "Ben Craddock", "", "", ""}, { "wheatdog", "Tim Liou", "http://stringbulbs.com/", "", ""}, { "williamchyr", "William Chyr", "http://williamchyr.com/", "", ""}, { "wonchun", "Won Chun", "https://twitter.com/won3d", "", ""}, }; typedef struct { char *Medium; char *Icon; char *WrittenName; } category_medium; category_medium CategoryMedium[] = { // medium icon written name { "admin", "🗹", "Administrivia"}, { "afk", "…" , "Away from Keyboard"}, { "authored", "🗪", "Chat Comment"}, // TODO(matt): Conditionally handle Chat vs Guest Comments { "blackboard", "🖌", "Blackboard"}, { "experience", "🍷", "Experience"}, { "hat", "🎩", "Hat"}, { "multimedia", "🎬", "Media Clip"}, { "owl", "🦉", "Owl of Shame"}, { "programming", "🖮", "Programming"}, // TODO(matt): Potentially make this configurable per project { "rant", "💢", "Rant"}, { "research", "📖", "Research"}, { "run", "🏃", "In-Game"}, // TODO(matt): Potentially make this configurable per project { "speech", "🗩", "Speech"}, { "trivia", "🎲", "Trivia"}, }; enum { NS_CALENDRICAL, NS_LINEAR, NS_SEASONAL, } numbering_schemes; typedef struct { char *ProjectID; char *FullName; char *Unit; // e.g. Day, Episode, Session int NumberingScheme; // numbering_schemes char *Medium; char *AltURLPrefix; // NOTE(matt): This currently just straight up replaces the ProjectID in the player pages' output directories } project_info; project_info ProjectInfo[] = { { "book", "Book Club", "Day", NS_LINEAR, "research", "" }, { "pcalc", "pcalc", "Day", NS_LINEAR, "programming", "" }, { "riscy", "RISCY BUSINESS", "Day", NS_LINEAR, "programming", "" }, { "chat", "Handmade Chat", "Day", NS_LINEAR, "speech", "" }, { "code", "Handmade Hero", "Day", NS_LINEAR, "programming", "day" }, { "intro-to-c", "Intro to C on Windows", "Day", NS_LINEAR, "programming", "day" }, { "misc", "Handmade Miscellany", "", NS_LINEAR, "admin", "" }, { "ray", "Handmade Ray", "Day", NS_LINEAR, "programming", "" }, { "hmdshow", "HandmadeDev Show", "", NS_SEASONAL, "speech", "" }, { "obbg", "Open Block Building Game", "Episode", NS_LINEAR, "programming", "" }, { "sysadmin", "SysAdmin", "Session", NS_LINEAR, "admin", "" }, }; #define ArrayCount(A) sizeof(A)/sizeof(*(A)) __attribute__ ((format (printf, 2, 3))) int CopyString(char Dest[], char *Format, ...) { int Length = 0; va_list Args; va_start(Args, Format); Length = vsprintf(Dest, Format, Args); va_end(Args); return Length; } int StringLength(char *String) { int i = 0; while(String[i]) { ++i; } return i; } void CopyBuffer(buffer *Dest, buffer *Src) { Src->Ptr = Src->Location; while(*Src->Ptr) { // TODO(matt) { if(Dest->Ptr - Dest->Location >= Dest->Size) { fprintf(stderr, "CopyBuffer: %s cannot accommodate %s\n", Dest->ID, Src->ID); __asm__("int3"); } } *Dest->Ptr++ = *Src->Ptr++; } *Dest->Ptr = '\0'; } int CopyStringNoFormat(char *Dest, char *String) { int Length = 0; while(*String) { *Dest++ = *String++; ++Length; } *Dest = '\0'; return Length; } // TODO(matt): Maybe do a version of this that takes a string as a Terminator int CopyStringNoFormatT(char *Dest, char *String, char Terminator) { int Length = 0; while(*String != Terminator) { *Dest++ = *String++; ++Length; } *Dest = '\0'; return Length; } __attribute__ ((format (printf, 2, 3))) void CopyStringToBuffer(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); // TODO(matt) { if(Length + (Dest->Ptr - Dest->Location) >= Dest->Size) { fprintf(stderr, "CopyStringToBuffer: %s cannot accommodate %d-character string:\n" "\n" "%s\n", Dest->ID, Length, Format); __asm__("int3"); } } Dest->Ptr += Length; } void CopyStringToBufferHTMLSafe(buffer *Dest, char *String) { while(*String) { if(Dest->Ptr - Dest->Location >= Dest->Size) { fprintf(stderr, "CopyStringToBufferHTMLSafe: %s cannot accommodate %d-character string\n", Dest->ID, StringLength(String)); __asm__("int3"); } switch(*String) { case '<': CopyStringToBuffer(Dest, "<"); String++; break; case '>': CopyStringToBuffer(Dest, ">"); String++; break; case '&': CopyStringToBuffer(Dest, "&"); String++; break; case '\"': CopyStringToBuffer(Dest, """); String++; break; case '\'': CopyStringToBuffer(Dest, "'"); String++; break; default: *Dest->Ptr++ = *String++; break; } } } void CopyStringToBufferJSONSafe(buffer *Dest, char *String) { while(*String) { if(Dest->Ptr - Dest->Location >= Dest->Size) { fprintf(stderr, "CopyStringToBufferHTMLSafe: %s cannot accommodate %d-character string\n", Dest->ID, StringLength(String)); __asm__("int3"); } switch(*String) { case '\\': case '\"': *Dest->Ptr++ = '\\'; default: *Dest->Ptr++ = *String++; break; } } } int StringsDiffer(char *A, char *B) // NOTE(matt): Two null-terminated strings { while(*A && *B && *A == *B) { ++A, ++B; } return *A - *B; } bool StringsDifferT(char *A, // NOTE(matt): Null-terminated string char *B, // NOTE(matt): Not null-terminated string (e.g. one mid-buffer) char Terminator // NOTE(matt): Caller definable terminator. Pass 0 to only match on the extent of A ) { 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; } } int MakeDir(char *Path) { // TODO(matt): Correctly check for permissions int i = StringLength(Path); int Ancestors = 0; while(mkdir(Path, 00755) == -1) { if(errno == EACCES) { return RC_ERROR_DIRECTORY; } while(Path[i] != '/' && i > 0) { --i; } ++Ancestors; Path[i] = '\0'; if(i == 0) { return RC_ERROR_DIRECTORY; } } while(Ancestors > 0) { while(Path[i] != '\0') { ++i; } Path[i] = '/'; --Ancestors; if((mkdir(Path, 00755)) == -1) { return RC_ERROR_DIRECTORY; } } return RC_SUCCESS; } void LogUsage(buffer *Buffer) { #if DEBUG char LogPath[256]; CopyString(LogPath, "%s/%s", Config.CacheDir, "buffers.log"); FILE *LogFile; if(!(LogFile = fopen(LogPath, "a+"))) { MakeDir(Config.CacheDir); if(!(LogFile = fopen(LogPath, "a+"))) { perror("LogUsage"); return; } } fprintf(LogFile, "%s,%ld,%d\n", Buffer->ID, Buffer->Ptr - Buffer->Location, Buffer->Size); fclose(LogFile); #endif } __attribute__ ((format (printf, 2, 3))) void LogError(int LogLevel, char *Format, ...) { if(Config.LogLevel >= LogLevel) { char LogPath[256]; CopyString(LogPath, "%s/%s", Config.CacheDir, "errors.log"); FILE *LogFile; if(!(LogFile = fopen(LogPath, "a+"))) { MakeDir(Config.CacheDir); if(!(LogFile = fopen(LogPath, "a+"))) { perror("LogUsage"); return; } } va_list Args; va_start(Args, Format); vfprintf(LogFile, Format, Args); va_end(Args); // TODO(matt): Include the LogLevel "string" and the current wall time fprintf(LogFile, "\n"); fclose(LogFile); } } void FreeBuffer(buffer *Buffer) { free(Buffer->Location); Buffer->Location = '\0'; #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Freed %s\n", Buffer->ID); fclose(MemLog); printf(" Freed %s\n", Buffer->ID); #endif } #if 0 #define ClaimBuffer(MemoryArena, Buffer, ID, Size) if(__ClaimBuffer(MemoryArena, Buffer, ID, Size))\ {\ fprintf(stderr, "%s:%d: MemoryArena cannot contain %s of size %d\n", __FILE__, __LINE__, ID, Size);\ hmml_free(&HMML);\ FreeBuffer(MemoryArena);\ return 1;\ }; #endif int ClaimBuffer(buffer *Buffer, char *ID, int Size) { if(MemoryArena.Ptr - MemoryArena.Location + Size > MemoryArena.Size) { return RC_ARENA_FULL; } Buffer->Location = (char *)MemoryArena.Ptr; Buffer->Size = Size; Buffer->ID = ID; MemoryArena.Ptr += Buffer->Size; *Buffer->Location = '\0'; Buffer->Ptr = Buffer->Location; #if DEBUG float PercentageUsed = (float)(MemoryArena.Ptr - MemoryArena.Location) / MemoryArena.Size * 100; printf(" ClaimBuffer(%s): %d\n" " Total ClaimedMemory: %ld (%.2f%%, leaving %ld free)\n\n", Buffer->ID, Buffer->Size, MemoryArena.Ptr - MemoryArena.Location, PercentageUsed, MemoryArena.Size - (MemoryArena.Ptr - MemoryArena.Location)); #endif return RC_SUCCESS; } void DeclaimBuffer(buffer *Buffer) { *Buffer->Location = '\0'; MemoryArena.Ptr -= Buffer->Size; float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100; #if DEBUG printf("DeclaimBuffer(%s)\n" " Used: %ld / %d (%.2f%%)\n" "\n" " Total ClaimedMemory: %ld\n\n", Buffer->ID, Buffer->Ptr - Buffer->Location, Buffer->Size, PercentageUsed, MemoryArena.Ptr - MemoryArena.Location); #endif LogUsage(Buffer); if(PercentageUsed >= 80.0f) { // TODO(matt): Implement either dynamically growing buffers, or phoning home to matt@handmadedev.org LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed); fprintf(stderr, "Warning: %s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed); } Buffer->Size = 0; } void RewindBuffer(buffer *Buffer) { #if DEBUG float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100; printf("Rewinding %s\n" " Used: %ld / %d (%.2f%%)\n\n", Buffer->ID, Buffer->Ptr - Buffer->Location, Buffer->Size, PercentageUsed); #endif Buffer->Ptr = Buffer->Location; } enum { TEMPLATE_INDEX, TEMPLATE_PLAYER, TEMPLATE_BESPOKE } templates; char * GetDirectoryPath(char *Filepath) { char *Ptr = Filepath + StringLength(Filepath) - 1; while(Ptr > Filepath && *Ptr != '/') { --Ptr; } if(Ptr == Filepath) { *Ptr++ = '.'; } *Ptr = '\0'; return Filepath; } char * GetBaseFilename(char *Filepath, char *Extension // Including the "." // Pass 0 to retain the whole file path, only without its parent directories ) { char *BaseFilename = Filepath + StringLength(Filepath) - 1; while(BaseFilename > Filepath && *BaseFilename != '/') { --BaseFilename; } if(*BaseFilename == '/') { ++BaseFilename; } BaseFilename[StringLength(BaseFilename) - StringLength(Extension)] = '\0'; return BaseFilename; } void ConstructTemplatePath(template *Template, int TemplateType) { // NOTE(matt): Bespoke template paths are set relative to: // in Project Edition: ProjectDir // in Single Edition: Parent directory of .hmml file if(Template->Metadata.Filename[0] != '/') { char Temp[256]; CopyString(Temp, Template->Metadata.Filename); char *Ptr = Template->Metadata.Filename; if(TemplateType == TEMPLATE_BESPOKE) { if(Config.Edition == EDITION_SINGLE) { Ptr += CopyString(Ptr, "%s/", GetDirectoryPath(Config.SingleHMMLFilePath)); } else { Ptr += CopyString(Ptr, "%s/", Config.ProjectDir); } } else { Ptr += CopyString(Ptr, "%s/", Config.TemplatesDir); } CopyString(Ptr, "%s", Temp); } } void Clear(char *String, int Size) { for(int i = 0; i < Size; ++i) { String[i] = 0; } } int InitTemplate(template **Template) { if(MemoryArena.Ptr - MemoryArena.Location + sizeof(template) > MemoryArena.Size) { return RC_ARENA_FULL; } *Template = (template *)MemoryArena.Ptr; Clear((*Template)->Metadata.Filename, 256); // NOTE(matt): template_metadata specifies Filename[256] (*Template)->Metadata.Validity = 0; (*Template)->Metadata.TagCount = 0; for(int i = 0; i < 16; ++i) // NOTE(matt): template_metadata specifies Tag[16] { (*Template)->Metadata.Tag[i].Offset = 0; (*Template)->Metadata.Tag[i].TagCode = 0; } MemoryArena.Ptr += sizeof(template); return RC_SUCCESS; } int ClaimTemplate(template **Template, char *Location, int TemplateType) { CopyString((*Template)->Metadata.Filename, Location); ConstructTemplatePath((*Template), TemplateType); if(TemplateType == TEMPLATE_BESPOKE) { fprintf(stderr, "\e[0;35mPacking\e[0m template: %s\n", (*Template)->Metadata.Filename); } FILE *File; if(!(File = fopen((*Template)->Metadata.Filename, "r"))) { LogError(LOG_ERROR, "Unable to open template %s: %s", (*Template)->Metadata.Filename, strerror(errno)); fprintf(stderr, "Unable to open template %s: %s\n", (*Template)->Metadata.Filename, strerror(errno)); Clear((*Template)->Metadata.Filename, 256); // NOTE(matt): template_metadata specifies Filename[256] return RC_ERROR_FILE; } fseek(File, 0, SEEK_END); (*Template)->Buffer.Size = ftell(File); if(MemoryArena.Ptr - MemoryArena.Location + (*Template)->Buffer.Size > MemoryArena.Size) { Clear((*Template)->Metadata.Filename, 256); // NOTE(matt): template_metadata specifies Filename[256] return RC_ARENA_FULL; } (*Template)->Buffer.Location = MemoryArena.Ptr; (*Template)->Buffer.Ptr = (*Template)->Buffer.Location; (*Template)->Buffer.ID = (*Template)->Metadata.Filename; fseek(File, 0, SEEK_SET); fread((*Template)->Buffer.Location, (*Template)->Buffer.Size, 1, File); fclose(File); MemoryArena.Ptr += (*Template)->Buffer.Size; #if DEBUG printf(" ClaimTemplate(%s): %d\n" " Total ClaimedMemory: %ld\n\n", (*Template)->Metadata.Filename, (*Template)->Buffer.Size, MemoryArena.Ptr - MemoryArena.Location); #endif return RC_SUCCESS; } int DeclaimTemplate(template *Template) { Clear(Template->Metadata.Filename, 256); Template->Metadata.Validity = 0; for(int i = 0; i < Template->Metadata.TagCount; ++i) { Template->Metadata.Tag[i].Offset = 0; Template->Metadata.Tag[i].TagCode = 0; } Template->Metadata.TagCount = 0; MemoryArena.Ptr -= (*Template).Buffer.Size; #if DEBUG printf("DeclaimTemplate(%s)\n" " Total ClaimedMemory: %ld\n\n", (*Template).Metadata.Filename, MemoryArena.Ptr - MemoryArena.Location); #endif return RC_SUCCESS; } 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; } hsl_colour * StringToColourHash(hsl_colour *Colour, char *String) { Colour->Hue = 0; Colour->Saturation = 0; Colour->Lightness = 74; int i; for(i = 0; String[i]; ++i) { Colour->Hue += CharToColour(String[i]).Hue; Colour->Saturation += CharToColour(String[i]).Saturation; } Colour->Hue = Colour->Hue % 360; Colour->Saturation = Colour->Saturation % 26 + 74; return(Colour); } char * SanitisePunctuation(char *String) { char *Ptr = String; while(*Ptr) { if(*Ptr == ' ') { *Ptr = '_'; } if((*Ptr < '0' || *Ptr > '9') && (*Ptr < 'a' || *Ptr > 'z') && (*Ptr < 'A' || *Ptr > 'Z')) { *Ptr = '-'; } ++Ptr; } return String; } enum { INCLUDE_CSS, INCLUDE_Images, INCLUDE_JS, } include_types; enum { PAGE_PLAYER = 1 << 0, PAGE_INDEX = 1 << 1 } pages; void ConstructURLPrefix(buffer *URLPrefix, int IncludeType, int PageType) { RewindBuffer(URLPrefix); if(StringsDiffer(Config.RootURL, "")) { URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "%s/", Config.RootURL); } else { if(Config.Edition == EDITION_PROJECT) { if(PageType == PAGE_PLAYER) { URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "../"); } URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "../"); } } switch(IncludeType) { case INCLUDE_CSS: if(StringsDiffer(Config.CSSDir, "")) { URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "%s/", Config.CSSDir); } break; case INCLUDE_Images: if(StringsDiffer(Config.ImagesDir, "")) { URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "%s/", Config.ImagesDir); } break; case INCLUDE_JS: if(StringsDiffer(Config.JSDir, "")) { URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "%s/", Config.JSDir); } break; } } enum { CreditsError_NoHost, CreditsError_NoAnnotator, CreditsError_NoCredentials } credits_errors; int SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char *Role) { bool Found = FALSE; for(int CredentialIndex = 0; CredentialIndex < ArrayCount(Credentials); ++CredentialIndex) { if(!StringsDiffer(Person, Credentials[CredentialIndex].Username)) { Found = TRUE; if(*HasCreditsMenu == FALSE) { CopyStringToBuffer(CreditsMenu, "
\n" "
\n" " Credits\n" "
\n"); *HasCreditsMenu = TRUE; } CopyStringToBuffer(CreditsMenu, " \n"); if(*Credentials[CredentialIndex].HomepageURL) { CopyStringToBuffer(CreditsMenu, " \n" "
%s
\n" "
%s
\n" "
\n", Credentials[CredentialIndex].HomepageURL, Role, Credentials[CredentialIndex].CreditedName); } else { CopyStringToBuffer(CreditsMenu, "
\n" "
%s
\n" "
%s
\n" "
\n", Role, Credentials[CredentialIndex].CreditedName); } if(*Credentials[CredentialIndex].SupportIcon && *Credentials[CredentialIndex].SupportURL) { buffer URLPrefix; ClaimBuffer(&URLPrefix, "URLPrefix", 1024); ConstructURLPrefix(&URLPrefix, INCLUDE_Images, PAGE_PLAYER); CopyStringToBuffer(CreditsMenu, "
\n", Credentials[CredentialIndex].SupportURL, URLPrefix.Location, Credentials[CredentialIndex].SupportIcon); DeclaimBuffer(&URLPrefix); } CopyStringToBuffer(CreditsMenu, "
\n"); } } return Found ? 0 : CreditsError_NoCredentials; } int BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Metadata) // TODO(matt): Make this take the Credentials, once we are parsing them from a config { if(Metadata->member) { if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->member, "Host")) { printf("No credentials for %s. Please contact matt@handmadedev.org with their:\n" " Full name\n" " Homepage URL (optional)\n" " Financial support info, e.g. Patreon URL (optional)\n", Metadata->member); } } else { if(*HasCreditsMenu == TRUE) { CopyStringToBuffer(CreditsMenu, "
\n" "
\n"); } return CreditsError_NoHost; } if(Metadata->co_host_count > 0) { for(int i = 0; i < Metadata->co_host_count; ++i) { if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->co_hosts[i], "Co-host")) { printf("No credentials for %s. Please contact matt@handmadedev.org with their:\n" " Full name\n" " Homepage URL (optional)\n" " Financial support info, e.g. Patreon URL (optional)\n", Metadata->co_hosts[i]); } } } if(Metadata->guest_count > 0) { for(int i = 0; i < Metadata->guest_count; ++i) { if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->guests[i], "Guest")) { printf("No credentials for %s. Please contact matt@handmadedev.org with their:\n" " Full name\n" " Homepage URL (optional)\n" " Financial support info, e.g. Patreon URL (optional)\n", Metadata->guests[i]); } } } if(Metadata->annotator_count > 0) { for(int i = 0; i < Metadata->annotator_count; ++i) { if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->annotators[i], "Annotator")) { printf("No credentials for %s. Please contact matt@handmadedev.org with their:\n" " Full name\n" " Homepage URL (optional)\n" " Financial support info, e.g. Patreon URL (optional)\n", Metadata->annotators[i]); } } } else { if(*HasCreditsMenu == TRUE) { CopyStringToBuffer(CreditsMenu, " \n" " \n"); } return CreditsError_NoAnnotator; } if(*HasCreditsMenu == TRUE) { CopyStringToBuffer(CreditsMenu, " \n" " \n"); } return 0; } int BuildReference(ref_info *ReferencesArray, int RefIdentifier, int UniqueRefs, HMML_Reference *Ref, HMML_Annotation *Anno) { #define REF_SITE (1 << 0) #define REF_PAGE (1 << 1) #define REF_URL (1 << 2) #define REF_TITLE (1 << 3) #define REF_ARTICLE (1 << 4) #define REF_AUTHOR (1 << 5) #define REF_EDITOR (1 << 6) #define REF_PUBLISHER (1 << 7) #define REF_ISBN (1 << 8) int Mask = 0; if(Ref->site) { Mask |= REF_SITE; } if(Ref->page) { Mask |= REF_PAGE; } if(Ref->url) { Mask |= REF_URL; } 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; } if(Ref->isbn) { Mask |= REF_ISBN; } if((REF_URL | REF_TITLE | REF_AUTHOR | REF_PUBLISHER | REF_ISBN) == Mask) { CopyString(ReferencesArray[UniqueRefs].ID, Ref->isbn); CopyString(ReferencesArray[UniqueRefs].Source, "%s (%s)", Ref->author, Ref->publisher); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->title); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url); } else if((REF_AUTHOR | REF_SITE | REF_PAGE | REF_URL) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url); CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, Ref->site); CopyString(ReferencesArray[UniqueRefs].RefTitle, "%s: \"%s\"", Ref->author, Ref->page); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url); } else if((REF_PAGE | REF_URL | REF_TITLE) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url); CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, Ref->title); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->page); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url); } else if((REF_SITE | REF_PAGE | REF_URL) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url); CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, Ref->site); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->page); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url); } else if((REF_SITE | REF_URL | REF_TITLE) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url); CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, Ref->site); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->title); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url); } else if((REF_TITLE | REF_AUTHOR | REF_ISBN) == Mask) { CopyString(ReferencesArray[UniqueRefs].ID, Ref->isbn); CopyString(ReferencesArray[UniqueRefs].Source, Ref->author); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->title); CopyString(ReferencesArray[UniqueRefs].URL, "http://www.openisbn.com/isbn/%s", Ref->isbn); } else if((REF_URL | REF_ARTICLE | REF_AUTHOR) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url); CopyString(ReferencesArray[UniqueRefs].Source, Ref->author); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->article); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url); } else if((REF_URL | REF_TITLE | REF_AUTHOR) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url); CopyString(ReferencesArray[UniqueRefs].Source, Ref->author); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->title); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url); } else if((REF_URL | REF_TITLE) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->title); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url); } else if((REF_SITE | REF_URL) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->site); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url); } else { return 1; } CopyString(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode, Anno->time); ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Identifier = RefIdentifier; return 0; } void InsertCategory(categories *GlobalTopics, categories *LocalTopics, categories *GlobalMedia, categories *LocalMedia, char *Marker) { bool IsMedium = FALSE; int CategoryMediumIndex; for(CategoryMediumIndex = 0; CategoryMediumIndex < ArrayCount(CategoryMedium); ++CategoryMediumIndex) { if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, Marker)) { IsMedium = TRUE; break; } } int CategoryIndex; if(IsMedium) { for(CategoryIndex = 0; CategoryIndex < LocalMedia->Count; ++CategoryIndex) { if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, LocalMedia->Category[CategoryIndex].Marker)) { return; } if((StringsDiffer(CategoryMedium[CategoryMediumIndex].WrittenName, LocalMedia->Category[CategoryIndex].WrittenText)) < 0) { int CategoryCount; for(CategoryCount = LocalMedia->Count; CategoryCount > CategoryIndex; --CategoryCount) { CopyString(LocalMedia->Category[CategoryCount].Marker, LocalMedia->Category[CategoryCount-1].Marker); CopyString(LocalMedia->Category[CategoryCount].WrittenText, LocalMedia->Category[CategoryCount-1].WrittenText); } CopyString(LocalMedia->Category[CategoryCount].Marker, CategoryMedium[CategoryMediumIndex].Medium); CopyString(LocalMedia->Category[CategoryCount].WrittenText, CategoryMedium[CategoryMediumIndex].WrittenName); break; } } if(CategoryIndex == LocalMedia->Count) { CopyString(LocalMedia->Category[CategoryIndex].Marker, CategoryMedium[CategoryMediumIndex].Medium); CopyString(LocalMedia->Category[CategoryIndex].WrittenText, CategoryMedium[CategoryMediumIndex].WrittenName); } ++LocalMedia->Count; for(CategoryIndex = 0; CategoryIndex < GlobalMedia->Count; ++CategoryIndex) { if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, GlobalMedia->Category[CategoryIndex].Marker)) { return; } if((StringsDiffer(CategoryMedium[CategoryMediumIndex].WrittenName, GlobalMedia->Category[CategoryIndex].WrittenText)) < 0) { int CategoryCount; for(CategoryCount = GlobalMedia->Count; CategoryCount > CategoryIndex; --CategoryCount) { CopyString(GlobalMedia->Category[CategoryCount].Marker, GlobalMedia->Category[CategoryCount-1].Marker); CopyString(GlobalMedia->Category[CategoryCount].WrittenText, GlobalMedia->Category[CategoryCount-1].WrittenText); } CopyString(GlobalMedia->Category[CategoryCount].Marker, CategoryMedium[CategoryMediumIndex].Medium); CopyString(GlobalMedia->Category[CategoryCount].WrittenText, CategoryMedium[CategoryMediumIndex].WrittenName); break; } } if(CategoryIndex == GlobalMedia->Count) { CopyString(GlobalMedia->Category[CategoryIndex].Marker, CategoryMedium[CategoryMediumIndex].Medium); CopyString(GlobalMedia->Category[CategoryIndex].WrittenText, CategoryMedium[CategoryMediumIndex].WrittenName); } ++GlobalMedia->Count; } else { for(CategoryIndex = 0; CategoryIndex < LocalTopics->Count; ++CategoryIndex) { if(!StringsDiffer(Marker, LocalTopics->Category[CategoryIndex].Marker)) { return; } if((StringsDiffer(Marker, LocalTopics->Category[CategoryIndex].Marker)) < 0) { int CategoryCount; for(CategoryCount = LocalTopics->Count; CategoryCount > CategoryIndex; --CategoryCount) { CopyString(LocalTopics->Category[CategoryCount].Marker, LocalTopics->Category[CategoryCount-1].Marker); } CopyString(LocalTopics->Category[CategoryCount].Marker, Marker); break; } } if(CategoryIndex == LocalTopics->Count) { CopyString(LocalTopics->Category[CategoryIndex].Marker, Marker); } ++LocalTopics->Count; for(CategoryIndex = 0; CategoryIndex < GlobalTopics->Count; ++CategoryIndex) { if(!StringsDiffer(Marker, GlobalTopics->Category[CategoryIndex].Marker)) { return; } if((StringsDiffer(Marker, GlobalTopics->Category[CategoryIndex].Marker)) < 0) { int CategoryCount; for(CategoryCount = GlobalTopics->Count; CategoryCount > CategoryIndex; --CategoryCount) { CopyString(GlobalTopics->Category[CategoryCount].Marker, GlobalTopics->Category[CategoryCount-1].Marker); } CopyString(GlobalTopics->Category[CategoryCount].Marker, Marker); break; } } if(CategoryIndex == GlobalTopics->Count) { CopyString(GlobalTopics->Category[CategoryIndex].Marker, Marker); } ++GlobalTopics->Count; } return; } void BuildCategories(buffer *AnnotationClass, buffer *TopicDots, categories *LocalTopics, categories *LocalMedia, int *MarkerIndex) { if(LocalTopics->Count > 0) { CopyStringToBuffer(TopicDots, ""); for(int i = 0; i < LocalTopics->Count; ++i) { CopyStringToBuffer(TopicDots, "
", SanitisePunctuation(LocalTopics->Category[i].Marker), SanitisePunctuation(LocalTopics->Category[i].Marker)); CopyStringToBuffer(AnnotationClass, " cat_%s", SanitisePunctuation(LocalTopics->Category[i].Marker)); } CopyStringToBuffer(TopicDots, "
"); } for(int i = 0; i < LocalMedia->Count; ++i) { if(!StringsDiffer(LocalMedia->Category[i].Marker, "afk")) // TODO(matt): Initially hidden config { CopyStringToBuffer(AnnotationClass, " off_%s skip", SanitisePunctuation(LocalMedia->Category[i].Marker)); // TODO(matt): Bulletproof this? } else { CopyStringToBuffer(AnnotationClass, " %s", SanitisePunctuation(LocalMedia->Category[i].Marker)); } } CopyStringToBuffer(AnnotationClass, "\""); } int StringToInt(char *String) { int Result = 0; while(*String) { Result = Result * 10 + (*String - '0'); ++String; } return Result; } size_t CurlIntoBuffer(char *InPtr, size_t CharLength, size_t Chars, char **OutputPtr) { int Length = CharLength * Chars; int i; for(i = 0; InPtr[i] && i < Length; ++i) { *((*OutputPtr)++) = InPtr[i]; } **OutputPtr = '\0'; return Length; }; void CurlQuotes(buffer *QuoteStaging, char *QuotesURL) { CURL *curl = curl_easy_init(); if(curl) { CURLcode res; curl_easy_setopt(curl, CURLOPT_WRITEDATA, &QuoteStaging->Ptr); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer); curl_easy_setopt(curl, CURLOPT_URL, QuotesURL); if((res = curl_easy_perform(curl))) { fprintf(stderr, "%s", curl_easy_strerror(res)); } curl_easy_cleanup(curl); } } int SearchQuotes(buffer *QuoteStaging, int CacheSize, quote_info *Info, int ID) { QuoteStaging->Ptr = QuoteStaging->Location; while(QuoteStaging->Ptr - QuoteStaging->Location < CacheSize) { char InID[4] = { 0 }; char InTime[16] = { 0 }; char *OutPtr = InID; QuoteStaging->Ptr += CopyStringNoFormatT(OutPtr, QuoteStaging->Ptr, ','); if(StringToInt(InID) == ID) { QuoteStaging->Ptr += 1; OutPtr = InTime; QuoteStaging->Ptr += CopyStringNoFormatT(OutPtr, QuoteStaging->Ptr, ','); long int Time = StringToInt(InTime); char DayString[3]; strftime(DayString, 3, "%d", gmtime(&Time)); int Day = StringToInt(DayString); char DaySuffix[3]; if(DayString[1] == '1' && Day != 11) { CopyString(DaySuffix, "st"); } else if(DayString[1] == '2' && Day != 12) { CopyString(DaySuffix, "nd"); } else if(DayString[1] == '3' && Day != 13) { CopyString(DaySuffix, "rd"); } else { CopyString(DaySuffix, "th"); } char MonthYear[32]; strftime(MonthYear, 32, "%B, %Y", gmtime(&Time)); CopyString(Info->Date, "%d%s %s", Day, DaySuffix, MonthYear); QuoteStaging->Ptr += 1; OutPtr = Info->Text; QuoteStaging->Ptr += CopyStringNoFormatT(OutPtr, QuoteStaging->Ptr, '\n'); FreeBuffer(QuoteStaging); return RC_FOUND; } else { while(*QuoteStaging->Ptr != '\n') { ++QuoteStaging->Ptr; } ++QuoteStaging->Ptr; } } return RC_UNFOUND; } int BuildQuote(quote_info *Info, char *Speaker, int ID) { // TODO(matt): Rebuild cache option char QuoteCacheDir[256]; CopyString(QuoteCacheDir, "%s/quotes", Config.CacheDir); char QuoteCachePath[256]; CopyString(QuoteCachePath, "%s/%s", QuoteCacheDir, Speaker); FILE *QuoteCache; char QuotesURL[256]; // TODO(matt): Make the URL configurable CopyString(QuotesURL, "https://dev.abaines.me.uk/quotes/%s.raw", Speaker); bool CacheAvailable = FALSE; if(!(QuoteCache = fopen(QuoteCachePath, "a+"))) { if(MakeDir(QuoteCacheDir) == RC_SUCCESS) { CacheAvailable = TRUE; }; if(!(QuoteCache = fopen(QuoteCachePath, "a+"))) { fprintf(stderr, "Unable to open quote cache %s: %s\n", QuoteCachePath, strerror(errno)); } else { CacheAvailable = TRUE; } } else { CacheAvailable = TRUE; } buffer QuoteStaging; QuoteStaging.ID = "QuoteStaging"; QuoteStaging.Size = Kilobytes(256); if(!(QuoteStaging.Location = malloc(QuoteStaging.Size))) { fclose(QuoteCache); return RC_ERROR_MEMORY; } #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Allocated QuoteStaging (%d)\n", QuoteStaging.Size); fclose(MemLog); printf(" Allocated QuoteStaging (%d)\n", QuoteStaging.Size); #endif QuoteStaging.Ptr = QuoteStaging.Location; if(CacheAvailable) { fseek(QuoteCache, 0, SEEK_END); int FileSize = ftell(QuoteCache); fseek(QuoteCache, 0, SEEK_SET); fread(QuoteStaging.Location, FileSize, 1, QuoteCache); fclose(QuoteCache); if(SearchQuotes(&QuoteStaging, FileSize, Info, ID) == RC_UNFOUND) { CurlQuotes(&QuoteStaging, QuotesURL); if(!(QuoteCache = fopen(QuoteCachePath, "w"))) { perror(QuoteCachePath); } fwrite(QuoteStaging.Location, QuoteStaging.Ptr - QuoteStaging.Location, 1, QuoteCache); fclose(QuoteCache); int CacheSize = QuoteStaging.Ptr - QuoteStaging.Location; QuoteStaging.Ptr = QuoteStaging.Location; if(SearchQuotes(&QuoteStaging, CacheSize, Info, ID) == RC_UNFOUND) { FreeBuffer(&QuoteStaging); return RC_UNFOUND; } } } else { CurlQuotes(&QuoteStaging, QuotesURL); int CacheSize = QuoteStaging.Ptr - QuoteStaging.Location; QuoteStaging.Ptr = QuoteStaging.Location; if(SearchQuotes(&QuoteStaging, CacheSize, Info, ID) == RC_UNFOUND) { FreeBuffer(&QuoteStaging); return RC_UNFOUND; } } return RC_SUCCESS; } int GenerateTopicColours(char *Topic) { for(int i = 0; i < ArrayCount(CategoryMedium); ++i) { if(!StringsDiffer(Topic, CategoryMedium[i].Medium)) { return RC_NOOP; } } file_buffer Topics; Topics.Buffer.ID = "Topics"; if(StringsDiffer(Config.CSSDir, "")) { CopyString(Topics.Path, "%s/%s/cinera_topics.css", Config.RootDir, Config.CSSDir); } else { CopyString(Topics.Path, "%s/cinera_topics.css", Config.RootDir); } char *Ptr = Topics.Path + StringLength(Topics.Path) - 1; while(*Ptr != '/') { --Ptr; } *Ptr = '\0'; DIR *CSSDirHandle; // TODO(matt): open() if(!(CSSDirHandle = opendir(Topics.Path))) { if(MakeDir(Topics.Path) == RC_ERROR_DIRECTORY) { LogError(LOG_ERROR, "Unable to create directory %s: %s", Topics.Path, strerror(errno)); fprintf(stderr, "Unable to create directory %s: %s\n", Topics.Path, strerror(errno)); return RC_ERROR_DIRECTORY; }; } closedir(CSSDirHandle); *Ptr = '/'; if((Topics.Handle = fopen(Topics.Path, "a+"))) { fseek(Topics.Handle, 0, SEEK_END); Topics.FileSize = ftell(Topics.Handle); Topics.Buffer.Size = Topics.FileSize; fseek(Topics.Handle, 0, SEEK_SET); if(!(Topics.Buffer.Location = malloc(Topics.Buffer.Size))) { return RC_ERROR_MEMORY; } #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Allocated Topics (%d)\n", Topics.Size); fclose(MemLog); printf(" Allocated Topics (%d)\n", Topics.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(SanitisePunctuation(Topic), 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; } hsl_colour Colour; StringToColourHash(&Colour, Topic); fprintf(Topics.Handle, ".category.%s { border: 1px solid hsl(%d, %d%%, %d%%); background: hsl(%d, %d%%, %d%%); }\n", SanitisePunctuation(Topic), Colour.Hue, Colour.Saturation, Colour.Lightness, Colour.Hue, Colour.Saturation, Colour.Lightness); fclose(Topics.Handle); FreeBuffer(&Topics.Buffer); return RC_SUCCESS; } else { // NOTE(matt): Maybe it shouldn't be possible to hit this case now that we MakeDir the actually dir... perror(Topics.Path); return RC_ERROR_FILE; } } void PrintUsage(char *BinaryLocation, config *DefaultConfig) { fprintf(stderr, "Usage: %s [option(s)] filename(s)\n" "\n" "Options:\n" " Paths: \e[1;30m(advisedly universal, but may be set per-(sub)project as required)\e[0m\n" " -r \n" " Override default root directory (\"%s\")\n" " -R \n" " Override default root URL (\"%s\")\n" " \e[1;31mIMPORTANT\e[0m: -r and -R must correspond to the same location\n" " \e[1;30mUNSUPPORTED: If you move files from RootDir, the RootURL should\n" " correspond to the resulting location\e[0m\n" "\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" "\n" " Project Settings:\n" " -p \n" " Set the project ID, equal to the \"project\" field in the HMML files\n" " NOTE: Setting the project ID triggers PROJECT EDITION\n" " -m \n" " Override default default medium (\"%s\")\n" " \e[1;30mKnown project defaults:\n", BinaryLocation, DefaultConfig->RootDir, DefaultConfig->RootURL, DefaultConfig->CSSDir, DefaultConfig->ImagesDir, DefaultConfig->JSDir, DefaultConfig->DefaultMedium); 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, "\e[0m -s \n" "\n" " \n" " \n", URLPrefix.Location, URLPrefix.Location, StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project, URLPrefix.Location, CINERA_APP_VERSION.Major, CINERA_APP_VERSION.Minor, CINERA_APP_VERSION.Patch); ConstructURLPrefix(&URLPrefix, INCLUDE_CSS, PAGE_PLAYER); CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "\n" " \n" " \n" "\n" " \n" " \n", URLPrefix.Location, URLPrefix.Location, StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project, URLPrefix.Location, CINERA_APP_VERSION.Major, CINERA_APP_VERSION.Minor, CINERA_APP_VERSION.Patch); if(Topics.Count || Media.Count) { CopyStringToBuffer(&CollationBuffers->IncludesPlayer, " 0) { for(int i = 0; i < Topics.Count; ++i) { CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "%s, ", Topics.Category[i].Marker); } } if(Media.Count > 0) { for(int i = 0; i < Media.Count; ++i) { CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "%s, ", Media.Category[i].WrittenText); } } CollationBuffers->IncludesPlayer.Ptr -= 2; CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "\">\n\n"); } ConstructURLPrefix(&URLPrefix, INCLUDE_JS, PAGE_PLAYER); CopyStringToBuffer(&CollationBuffers->IncludesPlayer, " \n", URLPrefix.Location); CopyStringToBuffer(&CollationBuffers->ScriptPlayer, " \n", URLPrefix.Location); DeclaimBuffer(&URLPrefix); if(HasFilterMenu) { CopyBuffer(&CollationBuffers->ScriptPlayer, &FilterState); } // NOTE(matt): Tree structure of "global" buffer dependencies // FilterState // CreditsMenu // FilterMedia // FilterTopics // FilterMenu // ReferenceMenu // QuoteMenu Cleanup: DeclaimBuffer(&FilterState); DeclaimBuffer(&CreditsMenu); DeclaimBuffer(&FilterMedia); DeclaimBuffer(&FilterTopics); DeclaimBuffer(&FilterMenu); DeclaimBuffer(&ReferenceMenu); DeclaimBuffer(&QuoteMenu); } else { LogError(LOG_ERROR, "%s:%d: %s", Filename, HMML.error.line, HMML.error.message); fprintf(stderr, "\e[1;31mSkipping\e[0m %s:%d: %s\n", Filename, HMML.error.line, HMML.error.message); hmml_free(&HMML); return RC_ERROR_HMML; } hmml_free(&HMML); return RC_SUCCESS; } int BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, int PageType) { #if DEBUG printf("\n\n --- Buffer Collation ---\n" " %s\n\n\n", OutputPath ? OutputPath : Config.OutLocation); #endif #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, "\nEntered BuffersToHTML(%s)\n", OutputPath ? OutputPath : Config->OutLocation); fclose(MemLog); #endif if(Template->Metadata.Filename && StringsDiffer(Template->Metadata.Filename, "")) { if((Template->Metadata.Filename && StringsDiffer(Template->Metadata.Filename, "")) && ((Template->Metadata.Validity & PageType) || Config.ForceIntegration)) { buffer Output; Output.Size = Template->Buffer.Size + (Kilobytes(512)); Output.ID = "Output"; if(!(Output.Location = malloc(Output.Size))) { LogError(LOG_ERROR, "BuffersToHTML(): %s", strerror(errno)); return RC_ERROR_MEMORY; } #if DEBUG_MEM MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Allocated Output (%d)\n", Output.Size); fclose(MemLog); printf(" Allocated Output (%d)\n", Output.Size); #endif Output.Ptr = Output.Location; Template->Buffer.Ptr = Template->Buffer.Location; for(int i = 0; i < Template->Metadata.TagCount; ++i) { int j = 0; while(Template->Metadata.Tag[i].Offset > j) { *Output.Ptr++ = *Template->Buffer.Ptr++; ++j; } switch(Template->Metadata.Tag[i].TagCode) { case TAG_PROJECT: if(CollationBuffers->ProjectName[0] == '\0') { fprintf(stderr, "Template contains a tag\n" "Skipping just this tag, because we do not know the project's full name\n"); } else { CopyStringToBuffer(&Output, CollationBuffers->ProjectName); } break; case TAG_TITLE: CopyStringToBuffer(&Output, CollationBuffers->Title); break; case TAG_URL: CopyStringToBuffer(&Output, PageType == PAGE_PLAYER ? CollationBuffers->URLPlayer : CollationBuffers->URLIndex); break; case TAG_VIDEO_ID: CopyStringToBuffer(&Output, CollationBuffers->VideoID); break; case TAG_INDEX: if(Config.Edition == EDITION_SINGLE) { fprintf(stderr, "Template contains a tag\n" "Skipping just this tag, because an index cannot be generated for inclusion in a\n" "bespoke template in Single Edition\n"); } else { CopyBuffer(&Output, &CollationBuffers->Index); } break; case TAG_INCLUDES: CopyBuffer(&Output, PageType == PAGE_PLAYER ? &CollationBuffers->IncludesPlayer : &CollationBuffers->IncludesIndex); break; case TAG_MENUS: CopyBuffer(&Output, &CollationBuffers->Menus); break; case TAG_PLAYER: CopyBuffer(&Output, &CollationBuffers->Player); break; case TAG_SCRIPT: CopyBuffer(&Output, &CollationBuffers->ScriptPlayer); break; } DepartComment(&Template->Buffer); } while(Template->Buffer.Ptr - Template->Buffer.Location < Template->Buffer.Size) { *Output.Ptr++ = *Template->Buffer.Ptr++; } FILE *OutFile; if(!(OutFile = fopen(Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutIntegratedLocation, "w"))) { LogError(LOG_ERROR, "Unable to open output file %s: %s", Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutIntegratedLocation, strerror(errno)); free(Output.Location); #if DEBUG_MEM MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Freed Output\n"); fclose(MemLog); printf(" Freed Output\n"); #endif return RC_ERROR_FILE; } fwrite(Output.Location, Output.Ptr - Output.Location, 1, OutFile); fclose(OutFile); free(Output.Location); #if DEBUG_MEM MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Freed Output\n"); fclose(MemLog); printf(" Freed Output\n"); #endif return RC_SUCCESS; } else { return RC_INVALID_TEMPLATE; } } else { buffer Master; if(ClaimBuffer(&Master, "Master", Kilobytes(512)) == RC_ARENA_FULL) { return RC_ARENA_FULL; }; CopyStringToBuffer(&Master, "\n" " \n"); CopyBuffer(&Master, PageType == PAGE_PLAYER ? &CollationBuffers->IncludesPlayer : &CollationBuffers->IncludesIndex); CopyStringToBuffer(&Master, "\n"); CopyStringToBuffer(&Master, " \n" " \n"); if(PageType == PAGE_PLAYER) { CopyBuffer(&Master, &CollationBuffers->Menus); CopyStringToBuffer(&Master, "\n"); CopyBuffer(&Master, &CollationBuffers->Player); CopyStringToBuffer(&Master, "\n"); CopyBuffer(&Master, &CollationBuffers->ScriptPlayer); CopyStringToBuffer(&Master, "\n"); } else { CopyBuffer(&Master, &CollationBuffers->Index); } CopyStringToBuffer(&Master, " \n" "\n"); FILE *OutFile; if(!(OutFile = fopen(Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutLocation, "w"))) { LogError(LOG_ERROR, "Unable to open output file %s: %s", Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutLocation, strerror(errno)); DeclaimBuffer(&Master); return RC_ERROR_FILE; } fwrite(Master.Location, Master.Ptr - Master.Location, 1, OutFile); fclose(OutFile); DeclaimBuffer(&Master); return RC_SUCCESS; } } int ReadFileIntoBuffer(file_buffer *File, int BufferPadding) { if(!(File->Handle = fopen(File->Path, "r"))) { return RC_ERROR_FILE; } fseek(File->Handle, 0, SEEK_END); File->FileSize = ftell(File->Handle); File->Buffer.Size = File->FileSize + 1 + BufferPadding; // NOTE(matt): +1 to accommodate a NULL terminator fseek(File->Handle, 0, SEEK_SET); // TODO(matt): Consider using the MemoryArena? Maybe have separate ReadFileIntoMemory() and ReadFileIntoArena() if(!(File->Buffer.Location = malloc(File->Buffer.Size))) { fclose(File->Handle); return RC_ERROR_MEMORY; } File->Buffer.Ptr = File->Buffer.Location; fread(File->Buffer.Location, File->FileSize, 1, File->Handle); File->Buffer.Location[File->FileSize] = '\0'; fclose(File->Handle); return RC_SUCCESS; } // NOTE(matt): Currently unused int WriteBufferToFile(file_buffer *File, buffer *Buffer, int BytesToWrite, bool KeepFileHandleOpen) { if(!(File->Handle = fopen(File->Path, "w"))) { return RC_ERROR_FILE; } fwrite(Buffer->Location, BytesToWrite, 1, File->Handle); if(!KeepFileHandleOpen) { fclose(File->Handle); } return RC_SUCCESS; } 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, int Direction, /* seek_directions */ int Position /* seek_positions */) { // 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; } // TODO(matt): Increment CINERA_DB_VERSION! typedef struct { unsigned int DBVersion; // NOTE(matt): Put this first to aid reliability version AppVersion; version HMMLVersion; unsigned int EntryCount; char IndexLocation[32]; char PlayerLocation[32]; } index_header; typedef struct { int Size; char BaseFilename[32]; } index_metadata; typedef struct { file_buffer File; file_buffer Metadata; index_header Header; index_metadata Entry; } index; // TODO(matt): Increment CINERA_DB_VERSION! int InsertIntoIndex(buffers *CollationBuffers, template **BespokeTemplate, char *BaseFilename) { // NOTE(matt): The index will be stored in two files: // 1) ProjectID.index // 2) ProjectID.metadata // // .index is all of the stuff needed for the search // .metadata is: int DBVersion // version AppVersion // version HMMLVersion // int EntryCount // for each Entry // int Size from start of "name:" to end of "---\n") // char BaseFilename[32] (0-padded) // index Index = { 0 }; Index.Metadata.Buffer.ID = "IndexMetadata"; CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID); int IndexMetadataFileReadCode = ReadFileIntoBuffer(&Index.Metadata, 0); switch(IndexMetadataFileReadCode) { case RC_ERROR_MEMORY: return RC_ERROR_MEMORY; case RC_ERROR_FILE: case RC_SUCCESS: break; } Index.File.Buffer.ID = "IndexFile"; CopyString(Index.File.Path, "%s/%s.index", Config.BaseDir, Config.ProjectID); int IndexFileReadCode = ReadFileIntoBuffer(&Index.File, 0); switch(IndexFileReadCode) { case RC_ERROR_MEMORY: return RC_ERROR_MEMORY; case RC_ERROR_FILE: case RC_SUCCESS: break; } int MetadataInsertionOffset = -1; int IndexEntryInsertionStart = -1; int IndexEntryInsertionEnd = -1; Index.Header.EntryCount = 0; char *IndexEntryStart; bool Found = FALSE; char InputFile[StringLength(BaseFilename) + StringLength(".hmml")]; CopyString(InputFile, "%s.hmml", BaseFilename); switch(HMMLToBuffers(CollationBuffers, BespokeTemplate, InputFile)) { // TODO(matt): Actually sort out the fatality of these cases, once we are always-on case RC_ERROR_FILE: case RC_ERROR_FATAL: return RC_ERROR_FATAL; case RC_ERROR_HMML: case RC_ERROR_MAX_REFS: case RC_ERROR_QUOTE: case RC_INVALID_REFERENCE: return RC_ERROR_HMML; case RC_SUCCESS: break; }; Index.Entry.Size = CollationBuffers->Search.Ptr - CollationBuffers->Search.Location; for(int i = 0; i < ArrayCount(Index.Entry.BaseFilename); ++i) { Index.Entry.BaseFilename[i] = '\0'; } CopyString(Index.Entry.BaseFilename, BaseFilename); int EntryIndex; if(IndexMetadataFileReadCode == RC_SUCCESS && IndexFileReadCode == RC_SUCCESS) { // TODO(matt): Index validation? // Maybe at least if(!StringsDiffer(..., "name: \""); // and check that we won't crash through the end of the file when skipping to the next entry Index.Header = *(index_header *)Index.Metadata.Buffer.Ptr; Index.Metadata.Buffer.Ptr += sizeof(Index.Header); Index.File.Buffer.Ptr += StringLength("---\n"); IndexEntryStart = Index.File.Buffer.Ptr; for(EntryIndex = 0; EntryIndex < Index.Header.EntryCount; ++EntryIndex) { index_metadata This = *(index_metadata *)Index.Metadata.Buffer.Ptr; if(!StringsDiffer(This.BaseFilename, BaseFilename)) { // Reinsert MetadataInsertionOffset = Index.Metadata.Buffer.Ptr - Index.Metadata.Buffer.Location; IndexEntryInsertionStart = IndexEntryStart - Index.File.Buffer.Location; IndexEntryInsertionEnd = IndexEntryInsertionStart + Index.Entry.Size; Found = TRUE; break; } else if(StringsDiffer(This.BaseFilename, BaseFilename) > 0) { // Insert MetadataInsertionOffset = Index.Metadata.Buffer.Ptr - Index.Metadata.Buffer.Location; IndexEntryInsertionStart = IndexEntryStart - Index.File.Buffer.Location; break; } else { Index.Metadata.Buffer.Ptr += sizeof(Index.Entry); IndexEntryStart += This.Size; Index.File.Buffer.Ptr = IndexEntryStart; } } } else { // NOTE(matt): Initialising new index_header Index.Header.DBVersion = CINERA_DB_VERSION; Index.Header.AppVersion = CINERA_APP_VERSION; Index.Header.HMMLVersion.Major = hmml_version.Major; Index.Header.HMMLVersion.Minor = hmml_version.Minor; Index.Header.HMMLVersion.Patch = hmml_version.Patch; CopyString(Index.Header.IndexLocation, Config.IndexLocation); CopyString(Index.Header.PlayerLocation, Config.PlayerLocation); DIR *OutputDirectoryHandle; if(!(OutputDirectoryHandle = opendir(Config.BaseDir))) { if(MakeDir(Config.BaseDir) == RC_ERROR_DIRECTORY) { LogError(LOG_ERROR, "Unable to create directory %s: %s", Config.BaseDir, strerror(errno)); fprintf(stderr, "Unable to create directory %s: %s\n", Config.BaseDir, strerror(errno)); return RC_ERROR_DIRECTORY; }; } closedir(OutputDirectoryHandle); } if(!(Index.Metadata.Handle = fopen(Index.Metadata.Path, "w"))) { FreeBuffer(&Index.Metadata.Buffer); return RC_ERROR_FILE; } if(!(Index.File.Handle = fopen(Index.File.Path, "w"))) { FreeBuffer(&Index.File.Buffer); return RC_ERROR_FILE; } if(!Found) { ++Index.Header.EntryCount; } fwrite(&Index.Header, sizeof(Index.Header), 1, Index.Metadata.Handle); if(IndexMetadataFileReadCode == RC_SUCCESS) { Index.Metadata.Buffer.Ptr = Index.Metadata.Buffer.Location + sizeof(Index.Header); } if(Found) { // NOTE(matt): We hit this during the start-up sync and when copying in a .hmml file over an already existing one, but // would need to fool about with the inotify event processing to get to this branch in the case that saving // a file triggers an IN_DELETE followed by an IN_CLOSE_WRITE event // Reinsert fwrite(Index.Metadata.Buffer.Ptr, MetadataInsertionOffset - sizeof(Index.Header), 1, Index.Metadata.Handle); fwrite(&Index.Entry, sizeof(Index.Entry), 1, Index.Metadata.Handle); fwrite(Index.Metadata.Buffer.Ptr - sizeof(Index.Header) + MetadataInsertionOffset + sizeof(Index.Entry), Index.Metadata.FileSize - MetadataInsertionOffset - sizeof(Index.Entry), 1, Index.Metadata.Handle); fwrite(Index.File.Buffer.Location, IndexEntryInsertionStart, 1, Index.File.Handle); fwrite(CollationBuffers->Search.Location, Index.Entry.Size, 1, Index.File.Handle); fwrite(Index.File.Buffer.Location + IndexEntryInsertionEnd, Index.File.FileSize - IndexEntryInsertionEnd, 1, Index.File.Handle); LogError(LOG_NOTICE, "Reinserted %s - %s", BaseFilename, CollationBuffers->Title); fprintf(stderr, "\e[1;33mReinserted\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title); } else if(MetadataInsertionOffset >= 0 && IndexEntryInsertionStart >= 0) { // Insert new fwrite(Index.Metadata.Buffer.Ptr, MetadataInsertionOffset - sizeof(Index.Header), 1, Index.Metadata.Handle); fwrite(&Index.Entry, sizeof(Index.Entry), 1, Index.Metadata.Handle); fwrite(Index.Metadata.Buffer.Ptr - sizeof(Index.Header) + MetadataInsertionOffset, Index.Metadata.FileSize - MetadataInsertionOffset, 1, Index.Metadata.Handle); fwrite(Index.File.Buffer.Location, IndexEntryInsertionStart, 1, Index.File.Handle); fwrite(CollationBuffers->Search.Location, Index.Entry.Size, 1, Index.File.Handle); fwrite(Index.File.Buffer.Location + IndexEntryInsertionStart, Index.File.FileSize - IndexEntryInsertionStart, 1, Index.File.Handle); LogError(LOG_NOTICE, "Inserted %s - %s", BaseFilename, CollationBuffers->Title); fprintf(stderr, "\e[1;32mInserted\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title); } else { // Append new if(IndexMetadataFileReadCode == RC_SUCCESS) { fwrite(Index.Metadata.Buffer.Ptr, Index.Metadata.FileSize - sizeof(Index.Header), 1, Index.Metadata.Handle); fwrite(Index.File.Buffer.Location, Index.File.FileSize, 1, Index.File.Handle); } else { fprintf(Index.File.Handle, "---\n"); } fwrite(&Index.Entry, sizeof(Index.Entry), 1, Index.Metadata.Handle); fwrite(CollationBuffers->Search.Location, Index.Entry.Size, 1, Index.File.Handle); LogError(LOG_NOTICE, "Appended %s - %s", BaseFilename, CollationBuffers->Title); fprintf(stderr, "\e[1;32mAppended\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title); } fclose(Index.Metadata.Handle); fclose(Index.File.Handle); FreeBuffer(&Index.Metadata.Buffer); FreeBuffer(&Index.File.Buffer); return RC_SUCCESS; } void ConstructDirectoryPath(buffer *DirectoryPath, int PageType, char *PageLocation, char *BaseFilename) { RewindBuffer(DirectoryPath); CopyStringToBuffer(DirectoryPath, Config.BaseDir); switch(PageType) { case PAGE_INDEX: if(StringsDiffer(PageLocation, "")) { CopyStringToBuffer(DirectoryPath, "/%s", PageLocation); } break; case PAGE_PLAYER: if(StringsDiffer(PageLocation, "")) { CopyStringToBuffer(DirectoryPath, "/%s", PageLocation); } if(StringsDiffer(Config.PlayerURLPrefix, "")) { char *Ptr = BaseFilename + StringLength(Config.ProjectID); CopyStringToBuffer(DirectoryPath, "/%s%s", Config.PlayerURLPrefix, Ptr); } else { CopyStringToBuffer(DirectoryPath, "/%s", BaseFilename); } break; } } int DeleteFromIndex(char *BaseFilename) { // TODO(matt): LogError() index Index; Index.Metadata.Buffer.ID = "IndexMetadata"; CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID); switch(ReadFileIntoBuffer(&Index.Metadata, 0)) { case RC_ERROR_FILE: return RC_ERROR_FILE; case RC_ERROR_MEMORY: LogError(LOG_ERROR, "DeleteFromIndex(): %s", strerror(errno)); return RC_ERROR_MEMORY; case RC_SUCCESS: break; } Index.File.Buffer.ID = "Index"; CopyString(Index.File.Path, "%s/%s.index", Config.BaseDir, Config.ProjectID); switch(ReadFileIntoBuffer(&Index.File, 0)) { case RC_ERROR_FILE: return RC_ERROR_FILE; case RC_ERROR_MEMORY: LogError(LOG_ERROR, "DeleteFromIndex(): %s", strerror(errno)); return RC_ERROR_MEMORY; case RC_SUCCESS: break; } Index.Header = *(index_header *)Index.Metadata.Buffer.Ptr; Index.Metadata.Buffer.Ptr += sizeof(Index.Header); bool Found = FALSE; int DeleteMetadataFrom = -1; int DeleteFileFrom = -1; int DeleteFileTo = -1; int SizeAcc = 0; for(int EntryIndex = 0; EntryIndex < Index.Header.EntryCount; ++EntryIndex, Index.Metadata.Buffer.Ptr += sizeof(index_metadata)) { index_metadata This = *(index_metadata *)Index.Metadata.Buffer.Ptr; if(!StringsDiffer(This.BaseFilename, BaseFilename)) { Found = TRUE; --Index.Header.EntryCount; DeleteMetadataFrom = Index.Metadata.Buffer.Ptr - Index.Metadata.Buffer.Location; DeleteFileFrom = StringLength("---\n") + SizeAcc; DeleteFileTo = DeleteFileFrom + This.Size; break; } SizeAcc += This.Size; } if(Found) { if(Index.Header.EntryCount == 0) { buffer IndexDirectory; ClaimBuffer(&IndexDirectory, "IndexDirectory", 1024); ConstructDirectoryPath(&IndexDirectory, PAGE_INDEX, Config.IndexLocation, ""); char IndexPagePath[1024]; CopyString(IndexPagePath, "%s/index.html", IndexDirectory.Location); remove(IndexPagePath); remove(IndexDirectory.Location); DeclaimBuffer(&IndexDirectory); remove(Index.Metadata.Path); remove(Index.File.Path); } else { if(!(Index.Metadata.Handle = fopen(Index.Metadata.Path, "w"))) { FreeBuffer(&Index.Metadata.Buffer); return RC_ERROR_FILE; } if(!(Index.File.Handle = fopen(Index.File.Path, "w"))) { FreeBuffer(&Index.File.Buffer); return RC_ERROR_FILE; } fwrite(&Index.Header, sizeof(Index.Header), 1, Index.Metadata.Handle); Index.Metadata.Buffer.Ptr = Index.Metadata.Buffer.Location + sizeof(Index.Header); fwrite(Index.Metadata.Buffer.Ptr, DeleteMetadataFrom - sizeof(Index.Header), 1, Index.Metadata.Handle); fwrite(Index.Metadata.Buffer.Ptr + DeleteMetadataFrom - sizeof(Index.Header) + sizeof(Index.Entry), Index.Metadata.FileSize - DeleteMetadataFrom - sizeof(Index.Entry), 1, Index.Metadata.Handle); fclose(Index.Metadata.Handle); fwrite(Index.File.Buffer.Location, DeleteFileFrom, 1, Index.File.Handle); fwrite(Index.File.Buffer.Location + DeleteFileTo, Index.File.FileSize - DeleteFileTo, 1, Index.File.Handle); fclose(Index.File.Handle); } } FreeBuffer(&Index.Metadata.Buffer); FreeBuffer(&Index.File.Buffer); return Found ? RC_SUCCESS : RC_NOOP; } int IndexToBuffer(buffers *CollationBuffers) // NOTE(matt): This guy malloc's CollationBuffers->Index { // TODO(matt): Consider parsing the index into a linked / skip list, or do something to save us having to iterate through // the index file multiple times index Index; Index.Metadata.Buffer.ID = "IndexMetadata"; CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID); int IndexMetadataFileReadCode = ReadFileIntoBuffer(&Index.Metadata, 0); Index.File.Buffer.ID = "IndexFile"; CopyString(Index.File.Path, "%s/%s.index", Config.BaseDir, Config.ProjectID); int IndexFileReadCode = ReadFileIntoBuffer(&Index.File, 0); if(IndexMetadataFileReadCode == RC_SUCCESS && IndexFileReadCode == RC_SUCCESS) { Index.Header = *(index_header*)Index.Metadata.Buffer.Ptr; Index.Metadata.Buffer.Ptr += sizeof(Index.Header); Index.File.Buffer.Ptr += StringLength("---\n"); char *IndexEntryStart = Index.File.Buffer.Ptr; bool ProjectFound = FALSE; int ProjectIndex; for(ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex) { if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID)) { ProjectFound = TRUE; break; } } if(!ProjectFound) { fprintf(stderr, "Missing Project Info for %s\n", Config.ProjectID); FreeBuffer(&Index.Metadata.Buffer); FreeBuffer(&Index.File.Buffer); return RC_ERROR_PROJECT; } int ThemeStringLength = StringsDiffer(Config.Theme, "") ? (StringLength(Config.Theme) * 2) : (StringLength(Config.ProjectID) * 2); char queryContainer[512 + ThemeStringLength]; CopyString(queryContainer, "
\n" " \n" "
\n" " \n" "
\n" " Downloading data...\n" "
\n" "
\n" "
\n" "
Found: 0 episodes, 0 markers, 0h 0m 0s total.
\n" "
\n" "\n" "
\n", StringsDiffer(Config.Theme, "") ? Config.Theme : Config.ProjectID, StringsDiffer(Config.Theme, "") ? Config.Theme : Config.ProjectID); buffer URLPrefix; ClaimBuffer(&URLPrefix, "URLPrefix", 1024); ConstructURLPrefix(&URLPrefix, INCLUDE_JS, PAGE_INDEX); buffer PlayerURL; ClaimBuffer(&PlayerURL, "PlayerURL", 4096); ConstructPlayerURL(&PlayerURL, ""); char Script[512 + StringLength(URLPrefix.Location) + (StringLength(Config.ProjectID) * 2)]; CopyString(Script, "
\n" " \n" " \n", Config.ProjectID, StringsDiffer(Config.Theme, "") ? Config.Theme : Config.ProjectID, Config.BaseURL, Config.PlayerLocation, StringsDiffer(Config.PlayerURLPrefix, "") ? Config.PlayerURLPrefix : Config.ProjectID, URLPrefix.Location); DeclaimBuffer(&URLPrefix); int EntryLength = 32 + StringLength(ProjectInfo[ProjectIndex].Unit) + 16 + 256; CollationBuffers->Index.Size = StringLength(queryContainer) + (Index.Header.EntryCount * EntryLength) + StringLength(Script); if(!(CollationBuffers->Index.Location = malloc(CollationBuffers->Index.Size))) { FreeBuffer(&Index.Metadata.Buffer); FreeBuffer(&Index.File.Buffer); return(RC_ERROR_MEMORY); } CollationBuffers->Index.Ptr = CollationBuffers->Index.Location; CopyStringToBuffer(&CollationBuffers->Index, queryContainer); for(int EntryIndex = 0; EntryIndex < Index.Header.EntryCount; ++EntryIndex) { index_metadata This = *(index_metadata *)Index.Metadata.Buffer.Ptr; char Number[16]; CopyString(Number, This.BaseFilename + StringLength(Config.ProjectID)); if(ProjectInfo[ProjectIndex].NumberingScheme == NS_LINEAR) { for(int i = 0; Number[i]; ++i) { if(Number[i] == '_') { Number[i] = '.'; } } } SeekBufferForString(&Index.File.Buffer, "title: \"", C_SEEK_FORWARDS, C_SEEK_AFTER); char Title[256]; CopyStringNoFormatT(Title, Index.File.Buffer.Ptr, '\n'); Title[StringLength(Title) - 1] = '\0'; ConstructPlayerURL(&PlayerURL, This.BaseFilename); if(StringsDiffer(ProjectInfo[ProjectIndex].Unit, "")) { CopyStringToBuffer(&CollationBuffers->Index, "
\n" " ", PlayerURL.Location); char Text[1024]; // NOTE(matt): Surely this will be big enough CopyString(Text, "%s %s: %s", ProjectInfo[ProjectIndex].Unit, // TODO(matt): Do we need to special-case the various numbering schemes? Number, Title); CopyStringToBufferHTMLSafe(&CollationBuffers->Index, Text); CopyStringToBuffer(&CollationBuffers->Index, "\n" "
\n"); } else { CopyStringToBuffer(&CollationBuffers->Index, "
\n" " %s\n" "
\n", PlayerURL.Location, Title); } Index.Metadata.Buffer.Ptr += sizeof(Index.Entry); IndexEntryStart += This.Size; Index.File.Buffer.Ptr = IndexEntryStart; } DeclaimBuffer(&PlayerURL); CopyStringToBuffer(&CollationBuffers->Index, Script); FreeBuffer(&Index.Metadata.Buffer); FreeBuffer(&Index.File.Buffer); return RC_SUCCESS; } else { return RC_ERROR_FILE; } } char * StripTrailingSlash(char *String) // NOTE(matt): For absolute paths { int Length = StringLength(String); while(Length > 0 && String[Length - 1] == '/') { String[Length - 1] = '\0'; --Length; } return String; } char * StripSurroundingSlashes(char *String) // NOTE(matt): For relative paths { int Length = StringLength(String); if(Length > 0) { while((String[0]) == '/') { ++String; --Length; } while(String[Length - 1] == '/') { String[Length - 1] = '\0'; --Length; } } return String; } int GeneratePlayerPage(buffers *CollationBuffers, template *PlayerTemplate, char *BaseFilename) { buffer OutputDirectoryPath; ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", 1024); ConstructDirectoryPath(&OutputDirectoryPath, PAGE_PLAYER, Config.PlayerLocation, BaseFilename); DIR *OutputDirectoryHandle; if(!(OutputDirectoryHandle = opendir(OutputDirectoryPath.Location))) // TODO(matt): open() { if(MakeDir(OutputDirectoryPath.Location) == RC_ERROR_DIRECTORY) { LogError(LOG_ERROR, "Unable to create directory %s: %s", OutputDirectoryPath.Location, strerror(errno)); fprintf(stderr, "Unable to create directory %s: %s\n", OutputDirectoryPath.Location, strerror(errno)); return RC_ERROR_DIRECTORY; }; } closedir(OutputDirectoryHandle); char PlayerPagePath[1024]; CopyString(PlayerPagePath, "%s/index.html", OutputDirectoryPath.Location); DeclaimBuffer(&OutputDirectoryPath); bool IndexInTemplate = FALSE; for(int TagIndex = 0; TagIndex < PlayerTemplate->Metadata.TagCount; ++TagIndex) { if(PlayerTemplate->Metadata.Tag[TagIndex].TagCode == TAG_INDEX) { IndexInTemplate = TRUE; IndexToBuffer(CollationBuffers); break; } } BuffersToHTML(CollationBuffers, PlayerTemplate, PlayerPagePath, PAGE_PLAYER); if(IndexInTemplate) { FreeBuffer(&CollationBuffers->Index); } return RC_SUCCESS; } int GenerateIndexPage(buffers *CollationBuffers, template *IndexTemplate) { buffer OutputDirectoryPath; ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", 1024); ConstructDirectoryPath(&OutputDirectoryPath, PAGE_INDEX, Config.IndexLocation, 0); DIR *OutputDirectoryHandle; if(!(OutputDirectoryHandle = opendir(OutputDirectoryPath.Location))) // TODO(matt): open() { if(MakeDir(OutputDirectoryPath.Location) == RC_ERROR_DIRECTORY) { LogError(LOG_ERROR, "Unable to create directory %s: %s", OutputDirectoryPath.Location, strerror(errno)); fprintf(stderr, "Unable to create directory %s: %s\n", OutputDirectoryPath.Location, strerror(errno)); return RC_ERROR_DIRECTORY; }; } closedir(OutputDirectoryHandle); char IndexPagePath[1024]; CopyString(IndexPagePath, "%s/index.html", OutputDirectoryPath.Location); DeclaimBuffer(&OutputDirectoryPath); IndexToBuffer(CollationBuffers); BuffersToHTML(CollationBuffers, IndexTemplate, IndexPagePath, PAGE_INDEX); FreeBuffer(&CollationBuffers->Index); return RC_SUCCESS; } int DeletePlayerPageFromFilesystem(char *BaseFilename, char *PlayerLocation, bool Relocating) { // NOTE(matt): Once we have the notion of an output filename format, we'll need to use that here buffer OutputDirectoryPath; ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", 1024); ConstructDirectoryPath(&OutputDirectoryPath, PAGE_PLAYER, PlayerLocation, BaseFilename); DIR *PlayerDir; if((PlayerDir = opendir(OutputDirectoryPath.Location))) // There is a directory for the Player, which there probably should be if not for manual intervention { char PlayerPagePath[256]; CopyString(PlayerPagePath, "%s/index.html", OutputDirectoryPath.Location); FILE *PlayerPage; if((PlayerPage = fopen(PlayerPagePath, "r"))) { fclose(PlayerPage); remove(PlayerPagePath); } closedir(PlayerDir); if((remove(OutputDirectoryPath.Location) == -1)) { LogError(LOG_NOTICE, "Mostly deleted %s. Unable to remove directory %s: %s", BaseFilename, OutputDirectoryPath.Location, strerror(errno)); fprintf(stderr, "\e[1;30mMostly deleted\e[0m %s. \e[1;31mUnable to remove directory\e[0m %s: %s", BaseFilename, OutputDirectoryPath.Location, strerror(errno)); } else { if(!Relocating) { LogError(LOG_INFORMATIONAL, "Deleted %s", BaseFilename); fprintf(stderr, "\e[1;30mDeleted\e[0m %s\n", BaseFilename); } } } DeclaimBuffer(&OutputDirectoryPath); return RC_SUCCESS; } void DeleteEntry(char *BaseFilename) { if(DeleteFromIndex(BaseFilename) == RC_SUCCESS) { DeletePlayerPageFromFilesystem(BaseFilename, Config.PlayerLocation, FALSE); } } int MonitorDirectory(buffers *CollationBuffers, template *IndexTemplate, template *PlayerTemplate, template *BespokeTemplate, int inotifyInstance, int WatchDescriptor) { #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, "\nCalled MonitorDirectory()\n"); fclose(MemLog); #endif buffer Events; // TODO(matt): Figure out the max size necessary for the Events buffer if(ClaimBuffer(&Events, "inotify Events", Kilobytes(1024)) == RC_ARENA_FULL) { return RC_ARENA_FULL; }; struct inotify_event *Event; int BytesRead; while((BytesRead = read(inotifyInstance, Events.Location, Events.Size)) != -1 && errno == EAGAIN && BytesRead > 0) { for(Events.Ptr = Events.Location; Events.Ptr < Events.Location + BytesRead && Events.Ptr - Events.Location < Events.Size; Events.Ptr += sizeof(struct inotify_event) + Event->len) { NextEvent: Event = (struct inotify_event *)Events.Ptr; char *Ptr; Ptr = Event->name; Ptr += (StringLength(Event->name) - StringLength(".hmml")); if(!(StringsDiffer(Ptr, ".hmml"))) { *Ptr = '\0'; char BaseFilename[256]; CopyString(BaseFilename, Event->name); *Ptr = '.'; // TODO(matt): Maybe handle IN_ALL_EVENTS if(Event->mask & IN_DELETE || Event->mask & IN_MOVED_FROM) { DeleteEntry(BaseFilename); } else { if(InsertIntoIndex(CollationBuffers, &BespokeTemplate, BaseFilename) == RC_SUCCESS) { if(BespokeTemplate->Metadata.Filename && StringsDiffer(BespokeTemplate->Metadata.Filename, "")) { GeneratePlayerPage(CollationBuffers, BespokeTemplate, BaseFilename); DeclaimTemplate(BespokeTemplate); } else { GeneratePlayerPage(CollationBuffers, PlayerTemplate, BaseFilename); } GenerateIndexPage(CollationBuffers, IndexTemplate); } else { if(Events.Ptr < Events.Location + BytesRead && Events.Ptr - Events.Location < Events.Size) { Events.Ptr += sizeof(struct inotify_event) + Event->len; goto NextEvent; } else { break; } } } } } } DeclaimBuffer(&Events); return RC_NOOP; } int RemoveDirectory(char *Path) { if((remove(Path) == -1)) { LogError(LOG_NOTICE, "Unable to remove directory %s: %s", Path, strerror(errno)); fprintf(stderr, "\e[1;30mUnable to remove directory\e[0m %s: %s\n", Path, strerror(errno)); return RC_ERROR_DIRECTORY; } else { LogError(LOG_INFORMATIONAL, "Deleted %s", Path); //fprintf(stderr, "\e[1;30mDeleted\e[0m %s\n", Path); return RC_SUCCESS; } } int RemoveDirectoryRecursively(char *Path) { if(RemoveDirectory(Path) == RC_ERROR_DIRECTORY) { return RC_ERROR_DIRECTORY; } char *Ptr = Path + StringLength(Path) - 1; while(Ptr > Path) { if(*Ptr == '/') { *Ptr = '\0'; if(RemoveDirectory(Path) == RC_ERROR_DIRECTORY) { return RC_ERROR_DIRECTORY; } } --Ptr; } return RC_SUCCESS; } int UpgradeDB(index *Index) { int DBVersion = Index->Header.DBVersion; switch(DBVersion) { case 1: { typedef struct { unsigned int DBVersion; version AppVersion; version HMMLVersion; unsigned int EntryCount; } index_header1; typedef struct { int Size; char BaseFilename[32]; } index_metadata1; typedef struct { file_buffer File; file_buffer Metadata; index_header1 Header; index_metadata1 Entry; } index1; index1 OldIndex = { 0 }; OldIndex.Header = *(index_header1 *)Index->Metadata.Buffer.Ptr; Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + sizeof(OldIndex.Header); Index->Header.DBVersion = CINERA_DB_VERSION; Index->Header.AppVersion = CINERA_APP_VERSION; Index->Header.HMMLVersion.Major = hmml_version.Major; Index->Header.HMMLVersion.Minor = hmml_version.Minor; Index->Header.HMMLVersion.Patch = hmml_version.Patch; Index->Header.EntryCount = OldIndex.Header.EntryCount; Clear(Index->Header.IndexLocation, sizeof(Index->Header.IndexLocation)); Clear(Index->Header.PlayerLocation, sizeof(Index->Header.PlayerLocation)); if(!(Index->Metadata.Handle = fopen(Index->Metadata.Path, "w"))) { FreeBuffer(&Index->Metadata.Buffer); return RC_ERROR_FILE; } fwrite(&Index->Header, sizeof(Index->Header), 1, Index->Metadata.Handle); Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + sizeof(OldIndex.Header); fwrite(Index->Metadata.Buffer.Ptr, Index->Metadata.FileSize - sizeof(OldIndex.Header), 1, Index->Metadata.Handle); Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location; fclose(Index->Metadata.Handle); FreeBuffer(&Index->Metadata.Buffer); if(ReadFileIntoBuffer(&Index->Metadata, 0) == RC_ERROR_FILE) { return RC_ERROR_FILE; } } break; } fprintf(stderr, "\n\e[1;32mUpgraded Cinera DB from %d to %d!\e[0m\n\n", DBVersion, Index->Header.DBVersion); return RC_SUCCESS; } typedef struct { bool Present; char ID[32]; } index_entry; // Metadata, unless we actually want to bolster this? int DeleteDeadIndexEntries() { // TODO(matt): More rigorously figure out who we should delete // Maybe compare the output directory and the input HMML names index Index = { 0 }; Index.Metadata.Buffer.ID = "IndexMetadata"; CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID); if(ReadFileIntoBuffer(&Index.Metadata, 0) == RC_ERROR_FILE) { return RC_ERROR_FILE; } Index.Header = *(index_header *)Index.Metadata.Buffer.Ptr; if(Index.Header.DBVersion != CINERA_DB_VERSION) { if(CINERA_DB_VERSION == 3) { fprintf(stderr, "\n\e[1;31mHandle conversion from CINERA_DB_VERSION %d to %d!\e[0m\n\n", Index.Header.DBVersion, CINERA_DB_VERSION); exit(RC_ERROR_FATAL); } UpgradeDB(&Index); } if(StringsDiffer(Index.Header.PlayerLocation, Config.PlayerLocation)) { buffer PlayerDirectory; ClaimBuffer(&PlayerDirectory, "PlayerDirectory", 1024); printf("\e[1;33mRelocating Player Page%s from %s to %s\e[0m\n", Index.Header.EntryCount > 1 ? "s" : "", (StringsDiffer(Index.Header.PlayerLocation, "") ? Index.Header.PlayerLocation : (StringsDiffer(Config.BaseDir, ".") ? Config.BaseDir : "\"Base Directory\"")), (StringsDiffer(Config.PlayerLocation, "") ? Config.PlayerLocation : (StringsDiffer(Config.BaseDir, ".") ? Config.BaseDir : "\"Base Directory\""))); Index.Metadata.Buffer.Ptr = Index.Metadata.Buffer.Location + sizeof(Index.Header); for(int EntryIndex = 0; EntryIndex < Index.Header.EntryCount; ++EntryIndex) { index_metadata This = *(index_metadata *)Index.Metadata.Buffer.Ptr; ConstructDirectoryPath(&PlayerDirectory, PAGE_PLAYER, Index.Header.PlayerLocation, This.BaseFilename); DeletePlayerPageFromFilesystem(This.BaseFilename, Index.Header.PlayerLocation, TRUE); Index.Metadata.Buffer.Ptr += sizeof(This); } DeclaimBuffer(&PlayerDirectory); RemoveDirectoryRecursively(Index.Header.PlayerLocation); Clear(Index.Header.PlayerLocation, sizeof(Index.Header.PlayerLocation)); CopyString(Index.Header.PlayerLocation, Config.PlayerLocation); if(!(Index.Metadata.Handle = fopen(Index.Metadata.Path, "w"))) { FreeBuffer(&Index.Metadata.Buffer); return RC_ERROR_FILE; } fwrite(&Index.Header, sizeof(Index.Header), 1, Index.Metadata.Handle); Index.Metadata.Buffer.Ptr = Index.Metadata.Buffer.Location + sizeof(Index.Header); fwrite(Index.Metadata.Buffer.Ptr, Index.Metadata.FileSize - sizeof(Index.Header), 1, Index.Metadata.Handle); Index.Metadata.Buffer.Ptr = Index.Metadata.Buffer.Location; fclose(Index.Metadata.Handle); } if(StringsDiffer(Index.Header.IndexLocation, Config.IndexLocation)) { printf("\e[1;33mRelocating Index Page from %s to %s\e[0m\n", (StringsDiffer(Index.Header.IndexLocation, "") ? Index.Header.IndexLocation : (StringsDiffer(Config.BaseDir, ".") ? Config.BaseDir : "\"Base Directory\"")), (StringsDiffer(Config.IndexLocation, "") ? Config.IndexLocation : (StringsDiffer(Config.BaseDir, ".") ? Config.BaseDir : "\"Base Directory\""))); buffer IndexDirectory; ClaimBuffer(&IndexDirectory, "IndexDirectory", 1024); ConstructDirectoryPath(&IndexDirectory, PAGE_INDEX, Index.Header.IndexLocation, ""); char IndexPagePath[2048] = { 0 }; CopyString(IndexPagePath, "%s/index.html", IndexDirectory.Location); remove(IndexPagePath); RemoveDirectoryRecursively(IndexDirectory.Location); DeclaimBuffer(&IndexDirectory); Clear(Index.Header.IndexLocation, sizeof(Index.Header.IndexLocation)); CopyString(Index.Header.IndexLocation, Config.IndexLocation); if(!(Index.Metadata.Handle = fopen(Index.Metadata.Path, "w"))) { FreeBuffer(&Index.Metadata.Buffer); return RC_ERROR_FILE; } fwrite(&Index.Header, sizeof(Index.Header), 1, Index.Metadata.Handle); Index.Metadata.Buffer.Ptr = Index.Metadata.Buffer.Location + sizeof(Index.Header); fwrite(Index.Metadata.Buffer.Ptr, Index.Metadata.FileSize - sizeof(Index.Header), 1, Index.Metadata.Handle); Index.Metadata.Buffer.Ptr = Index.Metadata.Buffer.Location; fclose(Index.Metadata.Handle); } Index.Metadata.Buffer.Ptr = Index.Metadata.Buffer.Location + sizeof(Index.Header); index_entry Entries[Index.Header.EntryCount]; for(int EntryIndex = 0; EntryIndex < Index.Header.EntryCount; ++EntryIndex) { index_metadata This = *(index_metadata *)Index.Metadata.Buffer.Ptr; CopyStringNoFormat(Entries[EntryIndex].ID, This.BaseFilename); Entries[EntryIndex].Present = FALSE; Index.Metadata.Buffer.Ptr += sizeof(This); } DIR *ProjectDirHandle; if(!(ProjectDirHandle = opendir(Config.ProjectDir))) { LogError(LOG_ERROR, "Unable to scan project directory %s: %s", Config.ProjectDir, strerror(errno)); fprintf(stderr, "Unable to scan project directory %s: %s\n", Config.ProjectDir, strerror(errno)); return RC_ERROR_DIRECTORY; } struct dirent *ProjectFiles; while((ProjectFiles = readdir(ProjectDirHandle))) { char *Ptr; Ptr = ProjectFiles->d_name; Ptr += (StringLength(ProjectFiles->d_name) - StringLength(".hmml")); if(!(StringsDiffer(Ptr, ".hmml"))) { *Ptr = '\0'; for(int i = 0; i < Index.Header.EntryCount; ++i) { if(!StringsDiffer(Entries[i].ID, ProjectFiles->d_name)) { Entries[i].Present = TRUE; break; } } } } closedir(ProjectDirHandle); bool Deleted = FALSE; for(int i = 0; i < Index.Header.EntryCount; ++i) { if(Entries[i].Present == FALSE) { Deleted = TRUE; DeleteEntry(Entries[i].ID); } } FreeBuffer(&Index.Metadata.Buffer); return Deleted ? RC_SUCCESS : RC_NOOP; } int SyncIndexWithInput(buffers *CollationBuffers, template *IndexTemplate, template *PlayerTemplate, template *BespokeTemplate) { bool Deleted = FALSE; if(DeleteDeadIndexEntries() == RC_SUCCESS) { Deleted = TRUE; } DIR *ProjectDirHandle; if(!(ProjectDirHandle = opendir(Config.ProjectDir))) { LogError(LOG_ERROR, "Unable to scan project directory %s: %s", Config.ProjectDir, strerror(errno)); fprintf(stderr, "Unable to scan project directory %s: %s\n", Config.ProjectDir, strerror(errno)); return RC_ERROR_DIRECTORY; } struct dirent *ProjectFiles; bool Inserted = FALSE; while((ProjectFiles = readdir(ProjectDirHandle))) { char *Ptr = ProjectFiles->d_name; Ptr += (StringLength(ProjectFiles->d_name) - StringLength(".hmml")); if(!(StringsDiffer(Ptr, ".hmml"))) { *Ptr = '\0'; if(InsertIntoIndex(CollationBuffers, &BespokeTemplate, ProjectFiles->d_name) == RC_SUCCESS) { if(BespokeTemplate->Metadata.Filename && StringsDiffer(BespokeTemplate->Metadata.Filename, "")) { GeneratePlayerPage(CollationBuffers, BespokeTemplate, ProjectFiles->d_name); DeclaimTemplate(BespokeTemplate); } else { GeneratePlayerPage(CollationBuffers, PlayerTemplate, ProjectFiles->d_name); } Inserted = TRUE; } } } closedir(ProjectDirHandle); if(Deleted || Inserted) { GenerateIndexPage(CollationBuffers, IndexTemplate); } return RC_SUCCESS; } void PrintVersions() { curl_version_info_data *CurlVersion = curl_version_info(CURLVERSION_NOW); printf("Cinera: %d.%d.%d\n" "Cinera DB: %d\n" "hmmlib: %d.%d.%d\n" "libcurl: %s\n", CINERA_APP_VERSION.Major, CINERA_APP_VERSION.Minor, CINERA_APP_VERSION.Patch, CINERA_DB_VERSION, hmml_version.Major, hmml_version.Minor, hmml_version.Patch, CurlVersion->version); } int main(int ArgC, char **Args) { // TODO(matt): Read all defaults from the config config DefaultConfig = { .RootDir = ".", .RootURL = "", .CSSDir = "", .ImagesDir = "", .JSDir = "", .TemplatesDir = ".", .TemplateIndexLocation = "", .TemplatePlayerLocation = "", .BaseDir = ".", .BaseURL = "", .IndexLocation = "", .PlayerLocation = "", // Should default to the ProjectID .Edition = EDITION_SINGLE, .LogLevel = LOG_EMERGENCY, .DefaultMedium = "programming", .Mode = 0, .OutLocation = "out.html", .OutIntegratedLocation = "out_integrated.html", .ForceIntegration = FALSE, .ProjectDir = ".", .ProjectID = "", .Theme = "", .UpdateInterval = 4, .PlayerURLPrefix = "" }; if(getenv("XDG_CACHE_HOME")) { CopyString(DefaultConfig.CacheDir, "%s/cinera", getenv("XDG_CACHE_HOME")); } else { CopyString(DefaultConfig.CacheDir, "%s/.cache/cinera", getenv("HOME")); } Config = DefaultConfig; if(ArgC < 2) { PrintUsage(Args[0], &DefaultConfig); return RC_RIP; } char CommandLineArg; while((CommandLineArg = getopt(ArgC, Args, "a:b:B:c:d:fhi:j:l:m:n:o:p:qr:R:s:t:U:vx:y:")) != -1) { switch(CommandLineArg) { case 'a': Config.PlayerLocation = StripSurroundingSlashes(optarg); break; case 'b': Config.BaseDir = StripTrailingSlash(optarg); break; case 'B': Config.BaseURL = StripTrailingSlash(optarg); break; case 'c': Config.CSSDir = StripSurroundingSlashes(optarg); break; case 'd': Config.ProjectDir = StripTrailingSlash(optarg); break; case 'f': Config.ForceIntegration = TRUE; break; case 'i': Config.ImagesDir = StripSurroundingSlashes(optarg); break; case 'j': Config.JSDir = StripSurroundingSlashes(optarg); break; case 'l': // TODO(matt): Make this actually take a string, rather than requiring the LogLevel number Config.LogLevel = StringToInt(optarg); break; case 'm': Config.DefaultMedium = optarg; break; case 'n': Config.IndexLocation = StripSurroundingSlashes(optarg); break; case 'o': Config.OutLocation = optarg; Config.OutIntegratedLocation = optarg; break; case 'p': Config.ProjectID = optarg; break; case 'q': Config.Mode |= MODE_ONESHOT; break; case 'r': Config.RootDir = StripTrailingSlash(optarg); break; case 'R': Config.RootURL = StripTrailingSlash(optarg); break; case 's': Config.Theme = optarg; break; case 't': Config.TemplatesDir = StripTrailingSlash(optarg); break; case 'U': Config.UpdateInterval = StringToInt(optarg); break; case 'v': PrintVersions(); return RC_SUCCESS; case 'x': Config.TemplateIndexLocation = optarg; break; case 'y': Config.TemplatePlayerLocation = optarg; break; case 'h': default: PrintUsage(Args[0], &DefaultConfig); return RC_SUCCESS; } } if(StringsDiffer(Config.ProjectID, "")) { Config.Edition = EDITION_PROJECT; for(int ProjectInfoIndex = 0; ProjectInfoIndex < ArrayCount(ProjectInfo); ++ProjectInfoIndex) { if(!StringsDiffer(Config.ProjectID, ProjectInfo[ProjectInfoIndex].ProjectID)) { if(StringsDiffer(ProjectInfo[ProjectInfoIndex].Medium, "")) { Config.DefaultMedium = ProjectInfo[ProjectInfoIndex].Medium; } if(StringsDiffer(ProjectInfo[ProjectInfoIndex].AltURLPrefix, "")) { Config.PlayerURLPrefix = ProjectInfo[ProjectInfoIndex].AltURLPrefix; } break; } } } if(!MediumExists(Config.DefaultMedium)) { // TODO(matt): We'll want to stick around when we have multiple projects configured exit(RC_RIP); } // NOTE(matt): Init MemoryArena (it is global) MemoryArena.Size = Megabytes(4); if(!(MemoryArena.Location = calloc(MemoryArena.Size, 1))) { LogError(LOG_EMERGENCY, "%s: %s", Args[0], strerror(errno)); return RC_RIP; } MemoryArena.Ptr = MemoryArena.Location; #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Allocated MemoryArena (%d)\n", MemoryArena.Size); fclose(MemLog); printf(" Allocated MemoryArena (%d)\n", MemoryArena.Size); #endif #if DEBUG printf("Allocated MemoryArena: %d\n\n", MemoryArena.Size); #endif // NOTE(matt): Tree structure of buffer dependencies // IncludesPlayer // Menus // Player // ScriptPlayer // // IncludesIndex // Index buffers CollationBuffers; if(ClaimBuffer(&CollationBuffers.IncludesPlayer, "IncludesPlayer", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&CollationBuffers.Menus, "Menus", Kilobytes(32)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&CollationBuffers.Player, "Player", Kilobytes(256)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&CollationBuffers.ScriptPlayer, "ScriptPlayer", Kilobytes(8)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&CollationBuffers.IncludesIndex, "IncludesIndex", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&CollationBuffers.Search, "Search", Kilobytes(32)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&CollationBuffers.ScriptIndex, "ScriptIndex", 256) == RC_ARENA_FULL) { goto RIP; }; // NOTE(matt): Templating // // Config will contain paths of multiple templates // App is running all the time, and picking up changes to the config as we go // If we find a new template, we first of all Init and Validate it // In our case here, we just want to straight up Init our three possible templates // and Validate the Index and Player templates if their locations are set template *IndexTemplate; InitTemplate(&IndexTemplate); template *PlayerTemplate; InitTemplate(&PlayerTemplate); template *BespokeTemplate; InitTemplate(&BespokeTemplate); if(StringsDiffer(Config.TemplatePlayerLocation, "")) { switch(ValidateTemplate(&PlayerTemplate, Config.TemplatePlayerLocation, TEMPLATE_PLAYER)) { case RC_INVALID_TEMPLATE: // Invalid template case RC_ERROR_FILE: // Could not load template case RC_ERROR_MEMORY: // Could not allocate memory for template goto RIP; case RC_SUCCESS: break; } } if(Config.Edition == EDITION_PROJECT && StringsDiffer(Config.TemplateIndexLocation, "")) { switch(ValidateTemplate(&IndexTemplate, Config.TemplateIndexLocation, TEMPLATE_INDEX)) { case RC_INVALID_TEMPLATE: // Invalid template case RC_ERROR_MEMORY: // Could not allocate memory for template case RC_ERROR_FILE: // Could not load template goto RIP; case RC_SUCCESS: break; } } // NOTE(matt) // // Single Edition == Loop over Args[FileIndex] // Project Edition == Loop over Config.ProjectDir // // Integrating or not if(Config.Edition == EDITION_PROJECT) { #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "w+"); fprintf(MemLog, "Entered Project Edition\n"); fclose(MemLog); #endif // TODO(matt): Also log these startup messages? PrintVersions(); printf( "\n" "Universal\n" " Cache Directory: \e[1;30m(XDG_CACHE_HOME)\e[0m\t%s\n" "\n" " Root\n" " Directory: \e[1;30m(-r)\e[0m\t\t\t%s\n" " URL: \e[1;30m(-R)\e[0m\t\t\t%s\n" " Paths relative to root\n" " CSS: \e[1;30m(-c)\e[0m\t\t\t%s\n" " Images: \e[1;30m(-i)\e[0m\t\t\t%s\n" " JS: \e[1;30m(-j)\e[0m\t\t\t%s\n" "\n" "Project\n" " ID: \e[1;30m(-p)\e[0m\t\t\t\t%s\n" " Default Medium: \e[1;30m(-m)\e[0m\t\t%s\n" " Style / Theme: \e[1;30m(-s)\e[0m\t\t\t%s\n" "\n" "Input Paths\n" " Annotations Directory: \e[1;30m(-d)\e[0m\t\t%s\n" " Templates Directory: \e[1;30m(-t)\e[0m\t\t%s\n" " Index Template: \e[1;30m(-x)\e[0m\t\t%s\n" " Player Template: \e[1;30m(-y)\e[0m\t\t%s\n" "\n" "Output Paths\n" " Base\n" " Directory: \e[1;30m(-b)\e[0m\t\t\t%s\n" " URL: \e[1;30m(-B)\e[0m\t\t\t%s\n" " Paths relative to base\n" " Index Page: \e[1;30m(-n)\e[0m\t\t\t%s\n" /* NOTE(matt): Here, I think, is where we'll split into sub-projects (...really?...) */ " Player Page(s): \e[1;30m(-a)\e[0m\t\t%s\n" " Player Page Prefix: \e[1;30m(hardcoded)\e[0m\t%s\n" "\n", Config.CacheDir, Config.RootDir, StringsDiffer(Config.RootURL, "") ? Config.RootURL : "[empty]", StringsDiffer(Config.CSSDir, "") ? Config.CSSDir : "(same as root)", StringsDiffer(Config.ImagesDir, "") ? Config.ImagesDir : "(same as root)", StringsDiffer(Config.JSDir, "") ? Config.JSDir : "(same as root)", Config.ProjectID, Config.DefaultMedium, StringsDiffer(Config.Theme, "") ? Config.Theme: Config.ProjectID, Config.ProjectDir, Config.TemplatesDir, StringsDiffer(Config.TemplateIndexLocation, "") ? Config.TemplateIndexLocation : "[none set]", StringsDiffer(Config.TemplatePlayerLocation, "") ? Config.TemplatePlayerLocation : "[none set]", Config.BaseDir, StringsDiffer(Config.BaseURL, "") ? Config.BaseURL : "[empty]", StringsDiffer(Config.IndexLocation, "") ? Config.IndexLocation : "(same as base)", StringsDiffer(Config.PlayerLocation, "") ? Config.PlayerLocation : "(directly descended from base)", StringsDiffer(Config.PlayerURLPrefix, "") ? Config.PlayerURLPrefix : Config.ProjectID); if((StringsDiffer(Config.IndexLocation, "") || StringsDiffer(Config.PlayerLocation, "")) && StringLength(Config.BaseURL) == 0) { printf("\e[1;33mPlease set a Project Base URL (-B) so we can output the Index / Player pages to\n" "locations other than the defaults\e[0m\n"); return(RC_SUCCESS); } printf("┌╼ Synchronising with annotation files in Project Input Directory ╾┐\n"); SyncIndexWithInput(&CollationBuffers, IndexTemplate, PlayerTemplate, BespokeTemplate); if(Config.Mode & MODE_ONESHOT) { goto RIP; } printf("\n┌╼ Monitoring Project Directory for \e[1;32mnew\e[0m, \e[1;33medited\e[0m and \e[1;30mdeleted\e[0m .hmml files ╾┐\n"); int inotifyInstance = inotify_init1(IN_NONBLOCK); // NOTE(matt): Do we want to also watch IN_DELETE_SELF events? int WatchDescriptor = inotify_add_watch(inotifyInstance, Config.ProjectDir, IN_CLOSE_WRITE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO); while(MonitorDirectory(&CollationBuffers, IndexTemplate, PlayerTemplate, BespokeTemplate, inotifyInstance, WatchDescriptor) != RC_ERROR_FATAL) { sleep(Config.UpdateInterval); } } else { if(optind == ArgC) { fprintf(stderr, "%s: requires at least one input .hmml file\n", Args[0]); PrintUsage(Args[0], &DefaultConfig); goto RIP; } NextFile: // TODO(matt): Just change the default output location so all these guys won't overwrite each other for(int FileIndex = optind; FileIndex < ArgC; ++FileIndex) { bool HasBespokeTemplate = FALSE; char *Ptr = Args[FileIndex]; Ptr += (StringLength(Args[FileIndex]) - StringLength(".hmml")); if(!(StringsDiffer(Ptr, ".hmml"))) { CopyString(Config.SingleHMMLFilePath, Args[FileIndex]); switch(HMMLToBuffers(&CollationBuffers, &BespokeTemplate, Args[FileIndex])) { // TODO(matt): Actually sort out the fatality of these cases, once we are always-on case RC_ERROR_FILE: case RC_ERROR_FATAL: goto RIP; case RC_ERROR_HMML: case RC_ERROR_MAX_REFS: case RC_ERROR_QUOTE: case RC_INVALID_REFERENCE: if(FileIndex < (ArgC - 1)) { goto NextFile; } else { goto RIP; } case RC_SUCCESS: break; }; HasBespokeTemplate = StringsDiffer(BespokeTemplate->Metadata.Filename, ""); switch(BuffersToHTML(&CollationBuffers, HasBespokeTemplate ? BespokeTemplate : PlayerTemplate, 0, PAGE_PLAYER)) { // TODO(matt): Actually sort out the fatality of these cases, once we are always-on case RC_INVALID_TEMPLATE: if(HasBespokeTemplate) { DeclaimTemplate(BespokeTemplate); } if(FileIndex < (ArgC - 1)) { goto NextFile; } case RC_ERROR_MEMORY: case RC_ERROR_FILE: case RC_ARENA_FULL: goto RIP; case RC_SUCCESS: #if 0 fprintf(stdout, "\e[1;32mWritten\e[0m %s\n", HasBespokeTemplate ? Config.OutIntegratedLocation : Config.OutLocation); #endif if(HasBespokeTemplate) { DeclaimTemplate(BespokeTemplate); } break; }; } } } if(StringsDiffer(PlayerTemplate->Metadata.Filename, "")) { DeclaimTemplate(PlayerTemplate); } if(Config.Edition == EDITION_PROJECT && StringsDiffer(IndexTemplate->Metadata.Filename, "")) { DeclaimTemplate(IndexTemplate); } DeclaimBuffer(&CollationBuffers.ScriptIndex); DeclaimBuffer(&CollationBuffers.Search); DeclaimBuffer(&CollationBuffers.IncludesIndex); DeclaimBuffer(&CollationBuffers.ScriptPlayer); DeclaimBuffer(&CollationBuffers.Player); DeclaimBuffer(&CollationBuffers.Menus); DeclaimBuffer(&CollationBuffers.IncludesPlayer); RIP: free(MemoryArena.Location); #if DEBUG_MEM MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Freed MemoryArena\n"); fclose(MemLog); printf(" Freed MemoryArena\n"); #endif }