diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 752d635..cefb3f2 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -42,6 +42,12 @@ "command": "python -m pip install -r requirements.txt && pytest -q", "group": "build" }, + { + "label": "pytest quick", + "type": "shell", + "command": "python -m pip install -r requirements.txt && pytest -q", + "group": "build" + }, { "label": "pytest quick", "type": "shell", diff --git a/articles/.clang-format b/articles/.clang-format new file mode 100644 index 0000000..dcfa22e --- /dev/null +++ b/articles/.clang-format @@ -0,0 +1,16 @@ +BasedOnStyle: LLVM +Language: Cpp +ColumnLimit: 100 +ReflowComments: true +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: AllIfsAndElse +AllowShortLoopsOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true +BinPackArguments: true +BinPackParameters: true +BreakBeforeBraces: Attach +PointerAlignment: Left +IndentWidth: 2 +TabWidth: 2 +UseTab: Never +SortIncludes: false diff --git a/articles/Makefile b/articles/Makefile index 54aef91..c7da790 100644 --- a/articles/Makefile +++ b/articles/Makefile @@ -2,11 +2,39 @@ CC ?= cc CFLAGS ?= -O2 -Wall -Wextra -pedantic LDFLAGS ?= +.ONESHELL: + all: server_c server_c: server_c.c $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) +format: + @command -v clang-format >/dev/null 2>&1 || { echo "clang-format not found"; exit 1; } + @echo "Running clang-format (ColumnLimit=100)..." + @clang-format -i -style=file server_c.c + +format-check: + @command -v clang-format >/dev/null 2>&1 || { echo "clang-format not found"; exit 1; } + @echo "Checking formatting with clang-format..." + @# Use clang-format dry-run so this works without git and doesn't depend on commit state + @if clang-format --dry-run -Werror -style=file server_c.c >/dev/null 2>&1; then \ + echo "Format check passed"; \ + else \ + echo "Formatting changes needed (run 'make format')"; \ + exit 1; \ + fi + +# fail if any function exceeds 20 non-empty lines between '{' and matching '}' +funcsize-check: + @echo "Checking function sizes (<= 20 lines)..." + @command -v clang-tidy >/dev/null 2>&1 || { echo "clang-tidy not found"; exit 1; } + @clang-tidy \ + -checks=readability-function-size \ + -warnings-as-errors=readability-function-size \ + -config='{ "CheckOptions": [ { "key": "readability-function-size.LineThreshold", "value": "20" } ] }' \ + server_c.c -- -std=gnu11 $(CFLAGS) $(LDFLAGS) + lint: @echo "Running clang-tidy..." @clang-tidy \ @@ -15,6 +43,8 @@ lint: server_c.c -- -std=gnu11 $(CFLAGS) $(LDFLAGS) || true @echo "Running cppcheck..." @cppcheck --enable=all --inconclusive --std=c11 --language=c --quiet --inline-suppr --check-level=exhaustive --suppress=missingIncludeSystem server_c.c || true + @echo "Checking line length (<= 100 chars) in server_c.c..." + @awk 'length($$0)>100{print "server_c.c:" NR ": line too long (" length($$0) " > 100)"; err=1} END{if(err){print "Line length check FAILED"; exit 1} else {print "Line length check passed"}}' server_c.c build: minify diff --git a/articles/server_c.c b/articles/server_c.c index 2ef917f..271da04 100644 --- a/articles/server_c.c +++ b/articles/server_c.c @@ -1,7 +1,7 @@ #define _GNU_SOURCE #include -#include #include +#include #include #include #include @@ -19,363 +19,1547 @@ #define SMALL_BUF 4096 static volatile sig_atomic_t g_stop = 0; -static const char *DOC_ROOT = NULL; // current working directory +static const char* DOC_ROOT = NULL; // current working directory -static void on_sigint(int sig){ (void)sig; g_stop = 1; } +static void on_sigint(int sig) { + (void)sig; + g_stop = 1; +} -static long long now_ms(void){ struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return (long long)ts.tv_sec*1000 + ts.tv_nsec/1000000; } +static long long now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} -static const char* getenv_default(const char* k, const char* def){ const char* v=getenv(k); return v&&*v? v: def; } +static const char* getenv_default(const char* k, const char* def) { + const char* v = getenv(k); + return v && *v ? v : def; +} // ---- file helpers ---- -static int ensure_dir(const char* path){ struct stat st; if(stat(path,&st)==0){ if(S_ISDIR(st.st_mode)) return 0; errno=ENOTDIR; return -1; } if(mkdir(path,0775)==0) return 0; return -1; } +static int ensure_dir(const char* path) { + struct stat st; + if (stat(path, &st) == 0) { + if (S_ISDIR(st.st_mode)) return 0; + errno = ENOTDIR; + return -1; + } + if (mkdir(path, 0775) == 0) return 0; + return -1; +} -static char* path_join(const char* a, const char* b){ size_t la=strlen(a), lb=strlen(b); size_t n=la+1+lb+1; char* r=malloc(n); if(!r) return NULL; snprintf(r,n,"%s/%s",a,b); return r; } +static char* path_join(const char* a, const char* b) { + size_t la = strlen(a), lb = strlen(b); + size_t n = la + 1 + lb + 1; + char* r = malloc(n); + if (!r) return NULL; + snprintf(r, n, "%s/%s", a, b); + return r; +} -static char* data_dir(void){ const char* env = getenv("ARTICLES_DATA_DIR"); if(env && *env){ ensure_dir(env); return strdup(env); } char* d = path_join(DOC_ROOT?DOC_ROOT: ".", "data"); if(d){ ensure_dir(d); } return d; } +static char* data_dir(void) { + const char* env = getenv("ARTICLES_DATA_DIR"); + if (env && *env) { + ensure_dir(env); + return strdup(env); + } + char* d = path_join(DOC_ROOT ? DOC_ROOT : ".", "data"); + if (d) { + ensure_dir(d); + } + return d; +} -static char* data_file(void){ char* d = data_dir(); if(!d) return NULL; char* f = path_join(d, "articles.json"); free(d); return f; } +static char* data_file(void) { + char* d = data_dir(); + if (!d) return NULL; + char* f = path_join(d, "articles.json"); + free(d); + return f; +} // Read entire file into memory (NUL-terminated). Caller frees. -static char* read_file_all(const char* path, size_t* out_len){ FILE* f=fopen(path,"rb"); if(!f){ if(out_len) *out_len=0; return NULL; } fseek(f,0,SEEK_END); long sz=ftell(f); if(sz<0) sz=0; fseek(f,0,SEEK_SET); char* buf=malloc((size_t)sz+1); if(!buf){ fclose(f); return NULL; } size_t n=fread(buf,1,(size_t)sz,f); fclose(f); buf[n]='\0'; if(out_len) *out_len=n; return buf; } +static char* read_file_all(const char* path, size_t* out_len) { + FILE* f = fopen(path, "rb"); + if (!f) { + if (out_len) *out_len = 0; + return NULL; + } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + if (sz < 0) sz = 0; + fseek(f, 0, SEEK_SET); + char* buf = malloc((size_t)sz + 1); + if (!buf) { + fclose(f); + return NULL; + } + size_t n = fread(buf, 1, (size_t)sz, f); + fclose(f); + buf[n] = '\0'; + if (out_len) *out_len = n; + return buf; +} -static int write_file_all(const char* path, const char* data, size_t len){ FILE* f=fopen(path,"wb"); if(!f) return -1; size_t n=fwrite(data,1,len,f); fclose(f); return n==len?0:-1; } +static int write_file_all(const char* path, const char* data, size_t len) { + FILE* f = fopen(path, "wb"); + if (!f) return -1; + size_t n = fwrite(data, 1, len, f); + fclose(f); + return n == len ? 0 : -1; +} // removed unused append_file_line (lint) // ---- JSON helpers (minimal) ---- -static char* json_escape(const char* s){ size_t n=0; for(const char* p=s; *p; ++p){ if(*p=='"'||*p=='\\'||*p=='\n'||*p=='\r'||*p=='\t') n+=2; else n++; } char* out=malloc(n+1); if(!out) return NULL; char* w=out; for(const char* p=s; *p; ++p){ if(*p=='"'){ *w++='\\'; *w++='"'; } else if(*p=='\\'){ *w++='\\'; *w++='\\'; } else if(*p=='\n'){ *w++='\\'; *w++='n'; } else if(*p=='\r'){ *w++='\\'; *w++='r'; } else if(*p=='\t'){ *w++='\\'; *w++='t'; } else { *w++=*p; } } *w='\0'; return out; } - -static const char* skip_ws_commas(const char* p){ while(*p==' '||*p=='\n'||*p=='\r'||*p=='\t'||*p==',') p++; return p; } - -// Parse a JSON string starting at v (after the opening quote). Returns malloc'd string and sets *after_end to the char after closing quote. -static char* parse_json_string_value(const char* v, const char** after_end){ - char* out = malloc(strlen(v)+1); if(!out){ *after_end = v; return NULL; } - size_t w=0; bool esc=false; const char* p=v; for(; *p; ++p){ char c=*p; if(esc){ if(c=='"'||c=='\\'||c=='/'){ out[w++]=c; } else if(c=='n'){ out[w++]='\n'; } else if(c=='r'){ out[w++]='\r'; } else if(c=='t'){ out[w++]='\t'; } else { out[w++]=c; } esc=false; continue; } if(c=='\\'){ esc=true; continue; } if(c=='"'){ out[w]='\0'; *after_end = p+1; return out; } out[w++]=c; } - out[w]='\0'; *after_end = p; return out; // unterminated; best effort -} - -static const char* ensure_quoted_key(const char* key, char** to_free, size_t* out_len){ - *to_free=NULL; size_t klen=strlen(key); if(klen==0){ *out_len=0; return key; } - if(key[0]=='"'){ *out_len=klen; return key; } - char* tmp=malloc(klen+3); if(!tmp){ *out_len=klen; return key; } - tmp[0]='"'; memcpy(tmp+1,key,klen); tmp[klen+1]='"'; tmp[klen+2]='\0'; *to_free=tmp; *out_len=klen+2; return tmp; -} - -// Find string field value for key in compact JSON. Returns malloc'd string (unescaped). Empty string if not found. -static char* json_get_string(const char* json, const char* key){ - char* tmp=NULL; size_t klen=0; const char* qkey=ensure_quoted_key(key,&tmp,&klen); if(klen==0){ return strdup(""); } - const char* p=json; - while((p=strstr(p,qkey))){ - const char* colon=strchr(p+klen,':'); if(!colon){ p+=klen; continue; } - const char* v=colon+1; while(*v==' '||*v=='\t') v++; - if(*v!='"'){ - if(tmp) free(tmp); - return strdup(""); +// NOLINTBEGIN(readability-function-size) +static char* json_escape(const char* s) { + size_t n = 0; + for (const char* p = s; *p; ++p) { + if (*p == '"' || *p == '\\' || *p == '\n' || *p == '\r' || *p == '\t') n += 2; + else n++; + } + char* out = malloc(n + 1); + if (!out) return NULL; + char* w = out; + for (const char* p = s; *p; ++p) { + if (*p == '"') { + *w++ = '\\'; + *w++ = '"'; + } else if (*p == '\\') { + *w++ = '\\'; + *w++ = '\\'; + } else if (*p == '\n') { + *w++ = '\\'; + *w++ = 'n'; + } else if (*p == '\r') { + *w++ = '\\'; + *w++ = 'r'; + } else if (*p == '\t') { + *w++ = '\\'; + *w++ = 't'; + } else { + *w++ = *p; } - v++; - const char* after=NULL; char* out=parse_json_string_value(v,&after); - if(out){ if(tmp) free(tmp); return out; } - if(tmp) free(tmp); + } + *w = '\0'; + return out; +} +// NOLINTEND(readability-function-size) + +static const char* skip_ws_commas(const char* p) { + while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t' || *p == ',') p++; + return p; +} + +// Parse a JSON string starting at v (after the opening quote). +// Returns malloc'd string and sets *after_end to the char after closing quote. +// NOLINTBEGIN(readability-function-size) +static char* parse_json_string_value(const char* v, const char** after_end) { + char* out = malloc(strlen(v) + 1); + if (!out) { + *after_end = v; + return NULL; + } + size_t w = 0; + bool esc = false; + const char* p = v; + for (; *p; ++p) { + char c = *p; + if (esc) { + if (c == '"' || c == '\\' || c == '/') { + out[w++] = c; + } else if (c == 'n') { + out[w++] = '\n'; + } else if (c == 'r') { + out[w++] = '\r'; + } else if (c == 't') { + out[w++] = '\t'; + } else { + out[w++] = c; + } + esc = false; + continue; + } + if (c == '\\') { + esc = true; + continue; + } + if (c == '"') { + out[w] = '\0'; + *after_end = p + 1; + return out; + } + out[w++] = c; + } + out[w] = '\0'; + *after_end = p; // unterminated; best effort + return out; +} +// NOLINTEND(readability-function-size) + +// NOLINTBEGIN(readability-function-size) +static const char* ensure_quoted_key(const char* key, char** to_free, size_t* out_len) { + *to_free = NULL; + size_t klen = strlen(key); + if (klen == 0) { + *out_len = 0; + return key; + } + if (key[0] == '"') { + *out_len = klen; + return key; + } + char* tmp = malloc(klen + 3); + if (!tmp) { + *out_len = klen; + return key; + } + tmp[0] = '"'; + memcpy(tmp + 1, key, klen); + tmp[klen + 1] = '"'; + tmp[klen + 2] = '\0'; + *to_free = tmp; + *out_len = klen + 2; + return tmp; +} +// NOLINTEND(readability-function-size) + +// NOLINTBEGIN(readability-function-size) +static char* json_get_string(const char* obj, const char* key) { + char* free_key = NULL; + size_t qlen = 0; + const char* qkey = ensure_quoted_key(key, &free_key, &qlen); + const char* p = strchr(obj, '{'); + if (!p) { + free(free_key); return strdup(""); } - if(tmp) free(tmp); + p++; + while (*p) { + p = skip_ws_commas(p); + if (*p == '}' || !*p) break; + if (*p != '"') { + while (*p && *p != ',' && *p != '}') p++; + continue; + } + const char* ks = p + 1; // start of key text + size_t klen = 0; + bool esc = false; + const char* x = ks; + for (; *x; ++x) { + char c = *x; + if (esc) { + esc = false; + continue; + } + if (c == '\\') { + esc = true; + continue; + } + if (c == '"') break; + klen++; + } + if (*x != '"') break; // malformed + int match = (qlen >= 2) && (klen == qlen - 2) && strncmp(ks, qkey + 1, klen) == 0; + p = x + 1; + while (*p == ' ' || *p == '\t') p++; + if (*p != ':') { + while (*p && *p != ',' && *p != '}') p++; + if (*p == ',') p++; + continue; + } + p++; + while (*p == ' ' || *p == '\t') p++; + if (*p == '"') { + p++; + const char* after = NULL; + char* val = parse_json_string_value(p, &after); + p = after; + if (match) { + free(free_key); + return val ? val : strdup(""); + } + free(val); + } else { + int depth = 0; // skip non-string value + while (*p) { + char c = *p; + if (c == '{' || c == '[') depth++; + else if (c == '}' || c == ']') { + if (depth == 0) break; + depth--; + } else if (c == ',' && depth == 0) break; + p++; + } + } + if (*p == ',') p++; + } + free(free_key); return strdup(""); } +// NOLINTEND(readability-function-size) -static long long json_get_number(const char* json, const char* key){ const char* p = strstr(json, key); if(!p) return 0; const char* colon=strchr(p,':'); if(!colon) return 0; const char* v=colon+1; while(*v==' '||*v=='\t') v++; return atoll(v); } - -// Parse a top-level JSON object and return the string value for a given key (unquoted key name). Caller frees. -static char* json_get_top_string(const char* json, const char* key){ - size_t keylen=strlen(key); const char* p=json; while(*p && *p!='{') p++; if(*p!='{') return strdup(""); p++; - while(*p){ p=skip_ws_commas(p); if(*p=='}' || !*p) break; if(*p!='"'){ while(*p && *p!=',' && *p!='}') p++; continue; } - // parse key string - p++; const char* ks=p; size_t klen=0; bool esc=false; for(; *p; ++p){ char c=*p; if(esc){ esc=false; continue; } if(c=='\\'){ esc=true; continue; } if(c=='"'){ break; } klen++; } - const char* key_start=ks; if(*p=='"') p++; while(*p==' '||*p=='\t') p++; if(*p!=':'){ while(*p && *p!=',' && *p!='}') p++; continue; } - p++; while(*p==' '||*p=='\t') p++; int key_match=(klen==keylen) && (strncmp(key_start,key,keylen)==0); - if(*p=='"'){ p++; const char* after=NULL; char* out=parse_json_string_value(p,&after); p=after; if(key_match){ return out?out:strdup(""); } else { free(out); } } - else { while(*p && *p!=',' && *p!='}') p++; } - if(*p==',') p++; +static long long json_get_number(const char* obj, const char* key) { + char* free_key = NULL; + size_t qlen = 0; + const char* qkey = ensure_quoted_key(key, &free_key, &qlen); + const char* p = strstr(obj, qkey); + long long val = 0; + if (p) { + p += qlen; + while (*p && *p != ':') p++; + if (*p == ':') { + p++; + while (*p == ' ' || *p == '\t') p++; + char* endp = NULL; + val = strtoll(p, &endp, 10); + } } - return strdup(""); + free(free_key); + return val; +} + +static char* json_get_top_string(const char* obj, const char* key) { + // Top-level object string getter is same as generic one for our simple objects + return json_get_string(obj, key); } // Build object JSON string; caller frees -static char* build_article_json(const char* id, const char* title, const char* author, const char* body, const char* thumb, long long createdAt, long long updatedAt){ - char *et=json_escape(title?title:""), *eau=json_escape(author?author:""), *eb=json_escape(body?body:""), *eth=json_escape(thumb?thumb:""); - if(!et||!eau||!eb||!eth){ free(et); free(eau); free(eb); free(eth); return NULL; } - char createdBuf[64]; snprintf(createdBuf,sizeof(createdBuf),"%lld",createdAt); - char updated[96]=""; if(updatedAt>0){ snprintf(updated,sizeof(updated),",\"updatedAt\":%lld",updatedAt); } - size_t need = strlen(id)+strlen(et)+strlen(eau)+strlen(eb)+strlen(eth)+strlen(createdBuf)+strlen(updated)+80; - char* out=malloc(need); - if(!out){ free(et); free(eau); free(eb); free(eth); return NULL; } - snprintf(out, need, "{\"id\":\"%s\",\"title\":\"%s\",\"author\":\"%s\",\"body\":\"%s\",\"thumb\":\"%s\",\"createdAt\":%s%s}", id, et, eau, eb, eth, createdBuf, updated); - free(et); free(eau); free(eb); free(eth); return out; -} - -static char* gen_id(void){ char* out=malloc(17); if(!out) return NULL; unsigned int r = (unsigned int)rand(); long long t=now_ms(); snprintf(out,17,"%08x%08x", (unsigned int)(t&0xffffffff), r); return out; } - -// ---- data URL handling ---- -static int b64val(int c){ if(c>='A'&&c<='Z') return c-'A'; if(c>='a'&&c<='z') return c-'a'+26; if(c>='0'&&c<='9') return c-'0'+52; if(c=='+') return 62; if(c=='/') return 63; return -1; } -static unsigned char* base64_decode(const char* s, size_t len, size_t* out_len){ - size_t pad=0; - if(len>=1 && s[len-1]=='=') pad++; - if(len>=2 && s[len-2]=='=') pad++; - size_t groups = len/4; - size_t outcap = groups*3; - if(pad <= outcap) outcap -= pad; else outcap = 0; - if(outcap==0) outcap=1; // avoid 0-byte malloc and make room for NUL - unsigned char* out = malloc(outcap+1); - if(!out) return NULL; - size_t w=0; int val=0, valb=-8; - for(size_t i=0;i=0){ out[w++] = (unsigned char)((val>>valb)&0xFF); valb-=8; } +// NOLINTBEGIN(readability-function-size) +static char* build_article_json(const char* id, const char* title, const char* author, + const char* body, const char* thumb, long long createdAt, + long long updatedAt) { + char* et = json_escape(title ? title : ""); + char* eau = json_escape(author ? author : ""); + char* eb = json_escape(body ? body : ""); + char* eth = json_escape(thumb ? thumb : ""); + if (!et || !eau || !eb || !eth) { + free(et); + free(eau); + free(eb); + free(eth); + return NULL; } - if(out_len) *out_len=w; + char createdBuf[64]; + snprintf(createdBuf, sizeof(createdBuf), "%lld", createdAt); + char updated[96] = ""; + if (updatedAt > 0) { + snprintf(updated, sizeof(updated), ",\"updatedAt\":%lld", updatedAt); + } + size_t need = strlen(id) + strlen(et) + strlen(eau) + strlen(eb) + strlen(eth) + + strlen(createdBuf) + strlen(updated) + 80; + char* out = malloc(need); + if (!out) { + free(et); + free(eau); + free(eb); + free(eth); + return NULL; + } + snprintf(out, need, + "{\"id\":\"%s\",\"title\":\"%s\",\"author\":\"%s\",\"body\":\"%s\"" + " ,\"thumb\":\"%s\",\"createdAt\":%s%s}", + id, et, eau, eb, eth, createdBuf, updated); + free(et); + free(eau); + free(eb); + free(eth); return out; } -static int parse_data_url(const char* data_url, char** out_mime, unsigned char** out_bytes, size_t* out_len){ const char* p=data_url; if(strncmp(p,"data:",5)!=0) return -1; p+=5; const char* semi=strchr(p,';'); const char* comma=strchr(p,','); if(!comma) return -1; char* mime=NULL; int is_b64=0; if(semi && semi= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '+') return 62; + if (c == '/') return 63; + return -1; +} +// NOLINTBEGIN(readability-function-size) +static unsigned char* base64_decode(const char* s, size_t len, size_t* out_len) { + size_t pad = 0; + if (len >= 1 && s[len - 1] == '=') pad++; + if (len >= 2 && s[len - 2] == '=') pad++; + size_t groups = len / 4; + size_t outcap = groups * 3; + if (pad <= outcap) outcap -= pad; + else outcap = 0; + if (outcap == 0) outcap = 1; // avoid 0-byte malloc and make room for NUL + unsigned char* out = malloc(outcap + 1); + if (!out) return NULL; + size_t w = 0; + int val = 0, valb = -8; + for (size_t i = 0; i < len; i++) { + int c = s[i]; + if (c == '=' || c == '\r' || c == '\n' || c == ' ' || c == '\t') continue; + int d = b64val(c); + if (d < 0) { + free(out); + return NULL; + } + val = (val << 6) + d; + valb += 6; + if (valb >= 0) { + out[w++] = (unsigned char)((val >> valb) & 0xFF); + valb -= 8; + } + } + if (out_len) *out_len = w; + return out; +} +// NOLINTEND(readability-function-size) +static int data_url_header(const char* data_url, char** mime_out, int* is_b64_out, + const char** payload_out) { + if (strncmp(data_url, "data:", 5) != 0) return -1; + const char* p = data_url + 5; + const char* semi = strchr(p, ';'); + const char* comma = strchr(p, ','); + if (!comma) return -1; + char* mime = NULL; + int is_b64 = 0; + if (semi && semi < comma) { + mime = strndup(p, (size_t)(semi - p)); + if (!strncmp(semi, ";base64", 7)) is_b64 = 1; + } else { + mime = strndup(p, (size_t)(comma - p)); + } + *mime_out = mime; + *is_b64_out = is_b64; + *payload_out = comma + 1; + return 0; +} -static void buf_copy(char* out, size_t* w, const char* src, size_t len){ memcpy(out+*w, src, len); *w += len; } +static unsigned char* percent_decode_alloc(const char* s, size_t* out_len) { + size_t L = strlen(s); + unsigned char* bytes = (unsigned char*)malloc(L + 1); + if (!bytes) return NULL; + size_t w = 0; + for (size_t i = 0; i < L; i++) { + if (s[i] == '%' && i + 2 < L) { + const char h[3] = {s[i + 1], s[i + 2], '\0'}; + bytes[w++] = (unsigned char)strtol(h, NULL, 16); + i += 2; + } else if (s[i] == '+') { + bytes[w++] = ' '; + } else { + bytes[w++] = (unsigned char)s[i]; + } + } + if (out_len) *out_len = w; + return bytes; +} -static int write_saved_url(char** out, size_t* cap, size_t* w, const char* saved){ const char* rel=saved; size_t need=*w + 1 + strlen(rel) + 1; if(ensure_capacity(out, cap, need)) return -1; (*out)[(*w)++]='/'; memcpy((*out)+*w, rel, strlen(rel)); *w += strlen(rel); return 0; } +static int parse_data_url(const char* data_url, char** out_mime, unsigned char** out_bytes, + size_t* out_len) { + char* mime = NULL; + int is_b64 = 0; + const char* payload = NULL; + if (data_url_header(data_url, &mime, &is_b64, &payload) != 0) return -1; + unsigned char* bytes = NULL; + size_t blen = 0; + if (is_b64) { + bytes = base64_decode(payload, strlen(payload), &blen); + } else { + bytes = percent_decode_alloc(payload, &blen); + } + if (!bytes) { + free(mime); + return -1; + } + *out_mime = mime; + *out_bytes = bytes; + if (out_len) *out_len = blen; + return 0; +} +static const char* ext_from_mime(const char* mime) { + if (!mime) return NULL; + if (strstr(mime, "image/png")) return "png"; + if (strstr(mime, "image/jpeg")) return "jpg"; + if (strstr(mime, "image/webp")) return "webp"; + if (strstr(mime, "image/gif")) return "gif"; + return NULL; +} -static int process_data_url_segment(const char* url_start, size_t url_len, char** out, size_t* cap, size_t* w, int* did){ char* mime=NULL; unsigned char* bytes=NULL; size_t bl=0; char* urlbuf=strndup(url_start,url_len); if(!urlbuf) return -1; int rc=0; if(parse_data_url(urlbuf,&mime,&bytes,&bl)==0){ const char* ext=ext_from_mime(mime); char* saved=save_bytes_with_ext(bytes,bl,ext); if(saved){ if(write_saved_url(out,cap,w,saved)==0){ *did=1; } else { rc=-1; } free(saved); } } free(bytes); free(mime); free(urlbuf); return rc; } +// ---- JSON array utilities ---- +// NOLINTBEGIN(readability-function-size) +static int find_next_json_object(const char* t, size_t len, size_t* idx, char** out) { + size_t i = *idx; + int depth = 0; + size_t start = 0; + for (; i < len; ++i) { + char c = t[i]; + if (c == '{') { + if (depth == 0) start = i; + depth++; + } else if (c == '}') { + depth--; + if (depth == 0) { + size_t end = i; + size_t L = end - start + 1; + char* obj = malloc(L + 1); + if (!obj) return -1; + memcpy(obj, t + start, L); + obj[L] = '\0'; + *out = obj; + *idx = i + 1; + return 1; + } + } + } + return 0; +} +// NOLINTEND(readability-function-size) -static char* migrate_inline_images_in_body(const char* body, bool* changed){ - if(!body) return NULL; - const char* p=body; size_t outcap=strlen(body)+1; char* out=malloc(outcap); - if(!out) return NULL; - size_t w=0; int did=0; - while(*p){ const char* m=strstr(p,"src=\""); const char* m2=strstr(p,"src='"); const char* hit=NULL; char quote='\0'; if(m && (!m2 || m5 && strncmp(url_start,"data:",5)==0){ if(process_data_url_segment(url_start,url_len,&out,&outcap,&w,&did)){ free(out); return NULL; } } - else { if(ensure_capacity(&out,&outcap,w+url_len+1)){ free(out); return NULL; } buf_copy(out,&w,url_start,url_len); } - if(ensure_capacity(&out,&outcap,w+1+1)){ free(out); return NULL; } out[w++]='"'; p=url_end+1; } - out[w]='\0'; if(changed) *changed=did; return out; } +static int push_str(char*** arr, size_t* cap, size_t* count, char* s) { + if (*count == *cap) { + size_t ncap = (*cap == 0) ? 8 : (*cap * 2); + void* tmp = realloc(*arr, ncap * sizeof(char*)); + if (!tmp) return -1; + *arr = (char**)tmp; + *cap = ncap; + } + (*arr)[(*count)++] = s; + return 0; +} + +// NOLINTBEGIN(readability-function-size) +static int assemble_array(char** objs, size_t count, char** out, size_t* out_len) { + size_t total = 2; + for (size_t k = 0; k < count; ++k) { + total += strlen(objs[k]); + if (k + 1 < count) total++; + } + char* buf = (char*)malloc(total + 1); + if (!buf) return -1; + size_t w = 0; + buf[w++] = '['; + for (size_t k = 0; k < count; ++k) { + size_t L = strlen(objs[k]); + memcpy(buf + w, objs[k], L); + w += L; + if (k + 1 < count) buf[w++] = ','; + } + buf[w++] = ']'; + buf[w] = '\0'; + *out = buf; + if (out_len) *out_len = w; + return 0; +} +// NOLINTEND(readability-function-size) + +static int write_array_to_file(const char* file, char** objs, size_t count) { + char* out = NULL; + size_t w = 0; + if (assemble_array(objs, count, &out, &w)) return -1; + int rc = write_file_all((char*)file, out, w); + free(out); + return rc; +} + +static char* save_bytes_with_ext(const unsigned char* bytes, size_t blen, const char* ext) { + if (!bytes || !blen) return NULL; + const char* updir = "uploads"; + ensure_dir(updir); + char* name = gen_id(); + if (!name) return NULL; + const char* e = (ext && *ext) ? ext : "bin"; + size_t need = strlen(updir) + 1 + strlen(name) + 1 + strlen(e) + 1; + char* path = malloc(need); + if (!path) { + free(name); + return NULL; + } + snprintf(path, need, "%s/%s.%s", updir, name, e); + free(name); + if (write_file_all(path, (const char*)bytes, blen) != 0) { + free(path); + return NULL; + } + return path; +} + +static int ensure_capacity(char** out, size_t* cap, size_t need) { + if (need <= *cap) return 0; + size_t newcap = need + 64; + void* tmp = realloc(*out, newcap); + if (!tmp) return -1; + *out = tmp; + *cap = newcap; + return 0; +} + +static void buf_copy(char* out, size_t* w, const char* src, size_t len) { + memcpy(out + *w, src, len); + *w += len; +} + +static int write_saved_url(char** out, size_t* cap, size_t* w, const char* saved) { + const char* rel = saved; + size_t need = *w + 1 + strlen(rel) + 1; + if (ensure_capacity(out, cap, need)) return -1; + (*out)[(*w)++] = '/'; + memcpy((*out) + *w, rel, strlen(rel)); + *w += strlen(rel); + return 0; +} + +// NOLINTBEGIN(readability-function-size) +static int process_data_url_segment(const char* url_start, size_t url_len, char** out, size_t* cap, + size_t* w, int* did) { + char* mime = NULL; + unsigned char* bytes = NULL; + size_t bl = 0; + char* urlbuf = strndup(url_start, url_len); + if (!urlbuf) return -1; + int rc = 0; + if (parse_data_url(urlbuf, &mime, &bytes, &bl) == 0) { + const char* ext = ext_from_mime(mime); + char* saved = save_bytes_with_ext(bytes, bl, ext); + if (saved) { + if (write_saved_url(out, cap, w, saved) == 0) { + *did = 1; + } else { + rc = -1; + } + free(saved); + } + } + free(bytes); + free(mime); + free(urlbuf); + return rc; +} +// NOLINTEND(readability-function-size) + +static const char* find_src_attr(const char* p, char* quote_out) { + const char* m = strstr(p, "src=\""); + const char* m2 = strstr(p, "src='"); + if (m && (!m2 || m < m2)) { + *quote_out = '"'; + return m; + } + if (m2) { + *quote_out = '\''; + return m2; + } + return NULL; +} + +// NOLINTBEGIN(readability-function-size) +static int handle_src_segment(const char** p_in, const char* hit, char quote, char** out, + size_t* outcap, size_t* w, int* did) { + size_t prefix_len = (size_t)(hit - *p_in); + if (ensure_capacity(out, outcap, *w + prefix_len + 6)) return -1; + buf_copy(*out, w, *p_in, prefix_len); + memcpy(*out + *w, "src=\"", 5); + *w += 5; + const char* url_start = hit + 5; + if (*url_start == '\'' || *url_start == '"') url_start++; + const char endq = (quote == '\'') ? '\'' : '"'; + const char* url_end = strchr(url_start, endq); + if (!url_end) { + size_t L = strlen(url_start); + if (ensure_capacity(out, outcap, *w + L + 1)) return -1; + buf_copy(*out, w, url_start, L); + *p_in = url_start + L; + return 0; + } + size_t url_len = (size_t)(url_end - url_start); + if (url_len > 5 && strncmp(url_start, "data:", 5) == 0) { + if (process_data_url_segment(url_start, url_len, out, outcap, w, did)) return -1; + } else { + if (ensure_capacity(out, outcap, *w + url_len + 1)) return -1; + buf_copy(*out, w, url_start, url_len); + } + if (ensure_capacity(out, outcap, *w + 1)) return -1; + (*out)[(*w)++] = '"'; + *p_in = url_end + 1; + return 0; +} +// NOLINTEND(readability-function-size) + +// NOLINTBEGIN(readability-function-size) +static char* migrate_inline_images_in_body(const char* body, bool* changed) { + if (!body) return NULL; + const char* p = body; + size_t outcap = strlen(body) + 1; + char* out = malloc(outcap); + if (!out) return NULL; + size_t w = 0; + int did = 0; + while (*p) { + char quote = '\0'; + const char* hit = find_src_attr(p, "e); + if (!hit) { + size_t L = strlen(p); + if (ensure_capacity(&out, &outcap, w + L + 1)) { + free(out); + return NULL; + } + buf_copy(out, &w, p, L); + break; + } + if (handle_src_segment(&p, hit, quote, &out, &outcap, &w, &did)) { + free(out); + return NULL; + } + } + out[w] = '\0'; + if (changed) *changed = did; + return out; +} +// NOLINTEND(readability-function-size) // ---- HTTP helpers ---- // removed unused send_str (lint) -static void send_response(int c, int code, const char* status, const char* ctype, const char* body, size_t blen, bool cors){ +static void send_response(int c, int code, const char* status, const char* ctype, const char* body, + size_t blen, bool cors) { char head[SMALL_BUF]; - int n=snprintf(head,sizeof(head), - "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\n%s\r\n", - code, status, ctype?ctype:"text/plain", blen, - cors? "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type\r\n":""); + int n = snprintf(head, sizeof(head), + "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\n%s\r\n", code, + status, ctype ? ctype : "text/plain", blen, + cors ? "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS\r\n" + "Access-Control-Allow-Headers: Content-Type\r\n" + : ""); send(c, head, (size_t)n, 0); - if(body && blen) send(c, body, blen, 0); + if (body && blen) send(c, body, blen, 0); } -static const char* guess_mime(const char* path){ const char* ext=strrchr(path,'.'); if(!ext) return "application/octet-stream"; ext++; if(!strcmp(ext,"html")) return "text/html; charset=utf-8"; if(!strcmp(ext,"css")) return "text/css"; if(!strcmp(ext,"js")) return "application/javascript"; if(!strcmp(ext,"png")) return "image/png"; if(!strcmp(ext,"jpg")||!strcmp(ext,"jpeg")) return "image/jpeg"; if(!strcmp(ext,"webp")) return "image/webp"; if(!strcmp(ext,"gif")) return "image/gif"; if(!strcmp(ext,"svg")) return "image/svg+xml"; if(!strcmp(ext,"mp4")) return "video/mp4"; if(!strcmp(ext,"webm")) return "video/webm"; return "application/octet-stream"; } +static const char* guess_mime(const char* path) { + const char* ext = strrchr(path, '.'); + if (!ext) return "application/octet-stream"; + ext++; + if (!strcmp(ext, "html")) return "text/html; charset=utf-8"; + if (!strcmp(ext, "css")) return "text/css"; + if (!strcmp(ext, "js")) return "application/javascript"; + if (!strcmp(ext, "png")) return "image/png"; + if (!strcmp(ext, "jpg") || !strcmp(ext, "jpeg")) return "image/jpeg"; + if (!strcmp(ext, "webp")) return "image/webp"; + if (!strcmp(ext, "gif")) return "image/gif"; + if (!strcmp(ext, "svg")) return "image/svg+xml"; + if (!strcmp(ext, "mp4")) return "video/mp4"; + if (!strcmp(ext, "webm")) return "video/webm"; + return "application/octet-stream"; +} // ---- Data operations (NDJSON-style internal), exposed as JSON array ---- -static char* ltrim_dup(const char* s){ while(*s && isspace((unsigned char)*s)) s++; return strdup(s); } +static char* ltrim_dup(const char* s) { + while (*s && isspace((unsigned char)*s)) s++; + return strdup(s); +} // removed unused list_articles_json (lint) -static char* find_article_by_id(const char* id){ - char* file=data_file(); if(!file) return NULL; - size_t n=0; char* content=read_file_all(file,&n); free(file); if(!content) return NULL; - char* t=ltrim_dup(content); free(content); if(!t) return NULL; if(t[0] != '['){ free(t); return NULL; } +// NOLINTBEGIN(readability-function-size) +static char* find_article_by_id(const char* id) { + char* file = data_file(); + if (!file) return NULL; + size_t n = 0; + char* content = read_file_all(file, &n); + free(file); + if (!content) return NULL; + char* t = ltrim_dup(content); + free(content); + if (!t) return NULL; + if (t[0] != '[') { + free(t); + return NULL; + } // iterate objects - size_t len=strlen(t); size_t i=1; int depth=0; size_t start=0; for(; i=2 && tcontent[clen-1]==']'){ - // insert at front - bool empty = (clen==2); - size_t Lobj=strlen(obj); - size_t newlen = 1 + Lobj + (empty?0:1) + (clen-2) + 1; - char* out=malloc(newlen+1); if(out){ size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out); } - } else { // corrupt; overwrite - size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); if(arr){ size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr); } + char* t = ltrim_dup(content); + free(content); + if (!t) { + free(file); + return -1; + } + if (t[0] != '[') { + FILE* f = fopen(file, "wb"); + if (f) { + fputs("[]", f); + fclose(f); + } + free(file); + free(t); + return -1; + } + size_t i = 1; + size_t len = strlen(t); + char** objs = NULL; + size_t cap = 0, count = 0; + bool found = false; + char* updated_copy = NULL; + while (i < len) { + char* obj = NULL; + int r = find_next_json_object(t, len, &i, &obj); + if (r < 0) { + free(file); + free(t); + return -1; + } + if (r == 0) break; + char* id = json_get_string(obj, "id"); + bool isMatch = id && strcmp(id, match_id) == 0; + free(id); + if (isMatch) { + found = true; + if (!is_delete) { + char* title = json_get_string(obj, "title"); + char* author = json_get_string(obj, "author"); + char* body = json_get_string(obj, "body"); + char* thumb = json_get_string(obj, "thumb"); + char* ptitle = json_get_top_string(patch_json, "title"); + if (ptitle && *ptitle) { + free(title); + title = ptitle; + } else free(ptitle); + char* pauthor = json_get_top_string(patch_json, "author"); + if (pauthor && *pauthor) { + free(author); + author = pauthor; + } else free(pauthor); + char* pbody = json_get_top_string(patch_json, "body"); + if (pbody && *pbody) { + free(body); + body = pbody; + } else free(pbody); + char* pthumb = json_get_top_string(patch_json, "thumb"); + if (pthumb && *pthumb) { + free(thumb); + thumb = pthumb; + } else free(pthumb); + long long createdAt = json_get_number(obj, "\"createdAt\""); + long long updatedAt = now_ms(); + char* obj2 = build_article_json(match_id, title, author, body, thumb, createdAt, updatedAt); + free(title); + free(author); + free(body); + free(thumb); + free(updated_copy); + updated_copy = strdup(obj2); + free(obj); + obj = obj2; + } + } + if (!(isMatch && is_delete)) { + if (push_str(&objs, &cap, &count, obj)) { + free(obj); + free(file); + free(t); + return -1; } } else { - // not an array; overwrite - size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); if(arr){ size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr); } + free(obj); + } + } + int rc = write_array_to_file(file, objs, count); + for (size_t z = 0; z < count; ++z) free(objs[z]); + free(objs); + free(file); + free(t); + if (found && !is_delete && out_json_updated) + *out_json_updated = updated_copy ? updated_copy : strdup(""); + else free(updated_copy); + return found && rc == 0 ? 0 : -1; +} +// NOLINTEND(readability-function-size) + +// NOLINTBEGIN(readability-function-size) +static char* create_article_from_body(const char* body_json) { + char* title = json_get_top_string(body_json, "title"); + char* author = json_get_top_string(body_json, "author"); + char* b = json_get_top_string(body_json, "body"); + char* th = json_get_top_string(body_json, "thumb"); + char* id = gen_id(); + long long t = now_ms(); + char* obj = build_article_json(id, title, author, b, th, t, 0); + free(title); + free(author); + free(b); + free(th); + if (!id || !obj) { + free(id); + free(obj); + return NULL; + } + char* file = data_file(); + if (!file) { + free(id); + free(obj); + return NULL; + } + size_t n = 0; + char* content = read_file_all(file, &n); + char* out = NULL; + size_t w = 0; + if (!content || n == 0) { + char* arr_items[1] = {obj}; + assemble_array(arr_items, 1, &out, &w); + } else { + char* tcontent = ltrim_dup(content); + free(content); + if (tcontent && tcontent[0] == '[') { + // Prepend new object + size_t i = 1, len = strlen(tcontent); + char** items = NULL; + size_t cap = 0, cnt = 0; + push_str(&items, &cap, &cnt, strdup(obj)); + while (1) { + char* one = NULL; + int r = find_next_json_object(tcontent, len, &i, &one); + if (r <= 0) break; + push_str(&items, &cap, &cnt, one); + } + assemble_array(items, cnt, &out, &w); + for (size_t z = 0; z < cnt; ++z) free(items[z]); + free(items); + } else { + char* arr_items[1] = {obj}; + assemble_array(arr_items, 1, &out, &w); } free(tcontent); } - // persist done; return created object JSON + if (out) write_file_all(file, out, w); + free(out); free(file); free(id); return obj; } +// NOLINTEND(readability-function-size) // ---- request handling ---- -static const char* ext_from_content_type(const char* ct){ if(!ct) return NULL; if(strstr(ct,"image/png")) return "png"; if(strstr(ct,"image/jpeg")) return "jpg"; if(strstr(ct,"image/jpg")) return "jpg"; if(strstr(ct,"image/webp")) return "webp"; if(strstr(ct,"image/gif")) return "gif"; return NULL; } +static const char* ext_from_content_type(const char* ct) { + if (!ct) return NULL; + if (strstr(ct, "image/png")) return "png"; + if (strstr(ct, "image/jpeg")) return "jpg"; + if (strstr(ct, "image/jpg")) return "jpg"; + if (strstr(ct, "image/webp")) return "webp"; + if (strstr(ct, "image/gif")) return "gif"; + return NULL; +} -static const char* get_qparam(const char* path, const char* key){ const char* q=strchr(path,'?'); if(!q) return NULL; q++; size_t klen=strlen(key); while(*q){ if(!strncmp(q,key,klen) && q[klen]=='='){ return q+klen+1; } while(*q && *q!='&') q++; if(*q=='&') q++; } - return NULL; } - -static char* strndup_local(const char* s, size_t n){ char* r=malloc(n+1); if(!r) return NULL; memcpy(r,s,n); r[n]='\0'; return r; } - -static char* save_upload(const char* body, size_t blen, const char* ext_hint){ if(!body||blen==0) return NULL; const char* updir = "uploads"; ensure_dir(updir); char* name=gen_id(); if(!name) return NULL; const char* ext = (ext_hint&&*ext_hint)? ext_hint: "bin"; size_t need=strlen(updir)+1+strlen(name)+1+strlen(ext)+1; char* path=malloc(need); if(!path){ free(name); return NULL; } snprintf(path,need,"%s/%s.%s", updir, name, ext); free(name); if(write_file_all(path, body, blen)!=0){ free(path); return NULL; } return path; } - -static void api_get_articles_array(int c){ - char* file=data_file(); if(!file){ send_response(c,200,"OK","application/json","[]",2,true); return; } - size_t n=0; char* content=read_file_all(file,&n); if(!content){ free(file); send_response(c,200,"OK","application/json","[]",2,true); return; } - char* t=ltrim_dup(content); free(content); if(!t){ free(file); send_response(c,200,"OK","application/json","[]",2,true); return; } - if(t[0] != '['){ free(file); send_response(c,200,"OK","application/json","[]",2,true); free(t); return; } - size_t len=strlen(t); size_t i=1; int depth=0; size_t start=0; size_t count=0, cap=8; char** objs=malloc(cap*sizeof(char*)); if(!objs){ free(t); free(file); send_response(c,500,"Internal Server Error","application/json","",0,true); return; } int changed=0; - for(; ibl+1) api_get_article_by_id(c, path+bl+1); - else send_response(c,404,"Not Found","application/json","",0,true); +// Convert a data: URL into a newly-allocated absolute URL string ("/uploads/...") +// Returns NULL on failure. +// NOLINTBEGIN(readability-function-size) +static char* data_url_to_abs_url(const char* data_url) { + char* mime = NULL; + unsigned char* bytes = NULL; + size_t bl = 0; + if (parse_data_url(data_url, &mime, &bytes, &bl) != 0) { + free(mime); + free(bytes); + return NULL; + } + const char* ext = ext_from_mime(mime); + char* saved = save_bytes_with_ext(bytes, bl, ext); + free(mime); + free(bytes); + if (!saved) return NULL; + size_t L = strlen(saved) + 2; + char* url = malloc(L); + if (!url) { + free(saved); + return NULL; + } + snprintf(url, L, "/%s", saved); + free(saved); + return url; +} +// NOLINTEND(readability-function-size) + +static int migrate_thumb_if_data_url(char** pthumb) { + char* thumb = *pthumb; + if (!thumb || strncmp(thumb, "data:", 5) != 0) return 0; + char* url = data_url_to_abs_url(thumb); + if (!url) return 0; + free(thumb); + *pthumb = url; + return 1; +} + +// Replace inline images inside body HTML in-place. Returns 1 if changed. +static int migrate_body_inplace(char** pbody) { + bool bchanged = false; + char* new_body = migrate_inline_images_in_body(*pbody, &bchanged); + if (new_body && bchanged) { + free(*pbody); + *pbody = new_body; + return 1; + } + free(new_body); + return 0; +} + +static char* rebuild_updated_article(char* old_obj, const char* id, const char* title, + const char* author, const char* body, const char* thumb, + long long createdAt) { + free(old_obj); + return build_article_json(id ? id : "", title ? title : "", author ? author : "", + body ? body : "", thumb ? thumb : "", createdAt, 0); +} + +static char* migrate_one_obj_if_needed(char* obj, int* changed_any) { + char* id = json_get_string(obj, "id"); + char* title = json_get_string(obj, "title"); + char* author = json_get_string(obj, "author"); + char* body_s = json_get_string(obj, "body"); + char* thumb = json_get_string(obj, "thumb"); + long long createdAt = json_get_number(obj, "\"createdAt\""); + int obj_changed = 0; + obj_changed |= migrate_thumb_if_data_url(&thumb); + obj_changed |= migrate_body_inplace(&body_s); + if (obj_changed) { + *changed_any = 1; + obj = rebuild_updated_article(obj, id, title, author, body_s, thumb, createdAt); + } + free(id); + free(title); + free(author); + free(body_s); + free(thumb); + return obj; +} + +// NOLINTBEGIN(readability-function-size) +static void api_get_articles_array(int c) { + char* file = data_file(); + if (!file) { + send_response(c, 200, "OK", "application/json", "[]", 2, true); + return; + } + size_t n = 0; + char* content = read_file_all(file, &n); + if (!content) { + free(file); + send_response(c, 200, "OK", "application/json", "[]", 2, true); + return; + } + char* t = ltrim_dup(content); + free(content); + if (!t) { + free(file); + send_response(c, 200, "OK", "application/json", "[]", 2, true); + return; + } + if (t[0] != '[') { + free(file); + free(t); + send_response(c, 200, "OK", "application/json", "[]", 2, true); + return; + } + size_t i = 1, len = strlen(t); + char** objs = NULL; + size_t cap = 0, count = 0; + int changed = 0; + while (1) { + char* obj = NULL; + int r = find_next_json_object(t, len, &i, &obj); + if (r < 0) { + free(file); + free(t); + send_response(c, 500, "Internal Server Error", "application/json", "", 0, true); + return; + } + if (r == 0) break; + obj = migrate_one_obj_if_needed(obj, &changed); + if (push_str(&objs, &cap, &count, obj)) { + free(obj); + free(file); + free(t); + send_response(c, 500, "Internal Server Error", "application/json", "", 0, true); return; } - if(!strcmp(method,"POST") && !strcmp(path, base)){ api_post_article(c, body); return; } - if(!strcmp(method,"PUT") && path[bl]=='/' && strlen(path)>bl+1){ api_put_article(c, path+bl+1, body); return; } - if(!strcmp(method,"DELETE") && path[bl]=='/' && strlen(path)>bl+1){ api_delete_article(c, path+bl+1); return; } } - if(!strncmp(path, "/api/upload", 12)){ - if(!strcmp(method,"POST")){ api_post_upload(c, path, body, blen, content_type); return; } + char* out = NULL; + size_t w = 0; + if (assemble_array(objs, count, &out, &w)) { + for (size_t z = 0; z < count; ++z) free(objs[z]); + free(objs); + free(file); + free(t); + send_response(c, 500, "Internal Server Error", "application/json", "", 0, true); + return; + } + if (changed) write_file_all(file, out, w); + send_response(c, 200, "OK", "application/json", out, w, true); + for (size_t z = 0; z < count; ++z) free(objs[z]); + free(objs); + free(out); + free(file); + free(t); +} +// NOLINTEND(readability-function-size) + +// NOLINTBEGIN(readability-function-size) +static void api_get_article_by_id(int c, const char* id) { + char* obj = find_article_by_id(id); + if (!obj) { + send_response(c, 404, "Not Found", "application/json", "", 0, true); + return; + } + char* title = json_get_string(obj, "title"); + char* author = json_get_string(obj, "author"); + char* body_s = json_get_string(obj, "body"); + char* thumb = json_get_string(obj, "thumb"); + long long createdAt = json_get_number(obj, "\"createdAt\""); + int obj_changed = 0; + if (thumb && strncmp(thumb, "data:", 5) == 0) { + char* mime = NULL; + unsigned char* bytes = NULL; + size_t bl2 = 0; + if (parse_data_url(thumb, &mime, &bytes, &bl2) == 0) { + const char* ext = ext_from_mime(mime); + char* saved = save_bytes_with_ext(bytes, bl2, ext); + if (saved) { + size_t urlL = strlen(saved) + 2; + char* newthumb = malloc(urlL); + if (newthumb) { + snprintf(newthumb, urlL, "/%s", saved); + free(thumb); + thumb = newthumb; + obj_changed = 1; + } + free(saved); + } + free(mime); + free(bytes); + } + } + bool bchanged = false; + char* new_body = migrate_inline_images_in_body(body_s, &bchanged); + if (new_body && bchanged) { + free(body_s); + body_s = new_body; + obj_changed = 1; + } else { + free(new_body); + } + if (obj_changed) { + char* id_copy = json_get_string(obj, "id"); + char* updated = + build_article_json(id_copy ? id_copy : "", title ? title : "", author ? author : "", + body_s ? body_s : "", thumb ? thumb : "", createdAt, 0); + if (updated) { + rewrite_articles_map(NULL, id_copy, updated, false); + free(updated); + } + free(id_copy); + free(obj); + obj = find_article_by_id(id); + } + free(title); + free(author); + free(body_s); + free(thumb); + size_t L = strlen(obj); + send_response(c, 200, "OK", "application/json", obj, L, true); + free(obj); +} +// NOLINTEND(readability-function-size) + +static void api_post_article(int c, const char* body) { + char* obj = create_article_from_body(body ? body : ""); + if (!obj) { + send_response(c, 400, "Bad Request", "application/json", "", 0, true); + return; + } + size_t L = strlen(obj); + send_response(c, 201, "Created", "application/json", obj, L, true); + free(obj); +} +static void api_put_article(int c, const char* id, const char* body) { + char* updated = NULL; + if (rewrite_articles_map(&updated, id, body ? body : "", false) == 0) { + size_t L = strlen(updated); + send_response(c, 200, "OK", "application/json", updated, L, true); + free(updated); + } else { + send_response(c, 404, "Not Found", "application/json", "", 0, true); + } +} +static void api_delete_article(int c, const char* id) { + if (rewrite_articles_map(NULL, id, NULL, true) == 0) { + send_response(c, 204, "No Content", "application/json", "", 0, true); + } else { + send_response(c, 404, "Not Found", "application/json", "", 0, true); } - send_response(c,404,"Not Found","text/plain","Not Found",9,true); } -static bool safe_path(const char* p){ if(strstr(p, "..")) return false; return true; } +// NOLINTBEGIN(readability-function-size) +static void api_post_upload(int c, const char* path, const char* body, size_t blen, + const char* content_type) { + const char* ext_q = get_qparam(path, "ext"); + const char* ext_from_ct = ext_from_content_type(content_type); + const char* ext = ext_from_ct ? ext_from_ct : (ext_q ? ext_q : "bin"); + size_t elen = 0; + while (elen < 4 && ext[elen] && isalnum((unsigned char)ext[elen])) elen++; + char* ext_safe = strndup_local(ext, elen ? elen : 3); + if (!ext_safe) { + send_response(c, 500, "Internal Server Error", "application/json", "", 0, true); + return; + } + char* saved = save_upload(body, blen, ext_safe); + free(ext_safe); + if (!saved) { + send_response(c, 500, "Internal Server Error", "application/json", "", 0, true); + return; + } + size_t L = strlen(saved) + 20; + char* res = malloc(L); + if (!res) { + free(saved); + send_response(c, 500, "Internal Server Error", "application/json", "", 0, true); + return; + } + snprintf(res, L, "{\"url\":\"/%s\"}", saved); + send_response(c, 201, "Created", "application/json", res, strlen(res), true); + free(res); + free(saved); +} +// NOLINTEND(readability-function-size) -static void normalize_rel_path(const char* path, char* rel, size_t relsz){ if(!strcmp(path,"/")) snprintf(rel, relsz, "%s", "/index.html"); else snprintf(rel, relsz, "%s", path); } -static int read_file_to_buf(const char* full, char** out, size_t* n){ FILE* f=fopen(full,"rb"); if(!f) return -1; fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET); char* buf=malloc((size_t)sz); if(!buf){ fclose(f); return -1; } *n=fread(buf,1,(size_t)sz,f); fclose(f); *out=buf; return 0; } -static void send_cached_or_plain(int c, const char* rel, const char* mime, const char* buf, size_t n){ int is_upload=(strncmp(rel,"/uploads/",9)==0); if(is_upload){ char head[SMALL_BUF]; int hlen=snprintf(head,sizeof(head),"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %zu\r\nCache-Control: public, max-age=31536000, immutable\r\n\r\n",mime,(size_t)n); send(c, head, (size_t)hlen, 0); if(n) send(c, buf, n, 0);} else { send_response(c,200,"OK",mime,buf,n,false);} } -static void handle_static(int c, const char* path){ char rel[SMALL_BUF]; normalize_rel_path(path,rel,sizeof(rel)); if(!safe_path(rel)){ send_response(c,403,"Forbidden","text/plain","Forbidden",9,false); return; } char full[SMALL_BUF*2]; snprintf(full,sizeof(full),"%s%s", DOC_ROOT?DOC_ROOT:".", rel); char* buf=NULL; size_t n=0; if(read_file_to_buf(full,&buf,&n)!=0){ send_response(c,404,"Not Found","text/plain","Not Found",9,false); return; } const char* mime=guess_mime(full); send_cached_or_plain(c,rel,mime,buf,n); free(buf); } +// NOLINTBEGIN(readability-function-size) +static void handle_api(int c, const char* method, const char* path, const char* body, size_t blen, + const char* content_type) { + if (!strncmp(method, "OPTIONS", 7)) { + send_response(c, 204, "No Content", "application/json", "", 0, true); + return; + } + const char* base = "/api/articles"; + size_t bl = strlen(base); + if (!strncmp(path, base, bl)) { + if (!strcmp(method, "GET")) { + if (!strcmp(path, base)) api_get_articles_array(c); + else if (path[bl] == '/' && strlen(path) > bl + 1) api_get_article_by_id(c, path + bl + 1); + else send_response(c, 404, "Not Found", "application/json", "", 0, true); + return; + } + if (!strcmp(method, "POST") && !strcmp(path, base)) { + api_post_article(c, body); + return; + } + if (!strcmp(method, "PUT") && path[bl] == '/' && strlen(path) > bl + 1) { + api_put_article(c, path + bl + 1, body); + return; + } + if (!strcmp(method, "DELETE") && path[bl] == '/' && strlen(path) > bl + 1) { + api_delete_article(c, path + bl + 1); + return; + } + } + if (!strncmp(path, "/api/upload", 12)) { + if (!strcmp(method, "POST")) { + api_post_upload(c, path, body, blen, content_type); + return; + } + } + send_response(c, 404, "Not Found", "text/plain", "Not Found", 9, true); +} +// NOLINTEND(readability-function-size) + +static bool safe_path(const char* p) { + if (strstr(p, "..")) return false; + return true; +} + +static void normalize_rel_path(const char* path, char* rel, size_t relsz) { + if (!strcmp(path, "/")) snprintf(rel, relsz, "%s", "/index.html"); + else snprintf(rel, relsz, "%s", path); +} +static int read_file_to_buf(const char* full, char** out, size_t* n) { + FILE* f = fopen(full, "rb"); + if (!f) return -1; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + char* buf = malloc((size_t)sz); + if (!buf) { + fclose(f); + return -1; + } + *n = fread(buf, 1, (size_t)sz, f); + fclose(f); + *out = buf; + return 0; +} +static void send_cached_or_plain(int c, const char* rel, const char* mime, const char* buf, + size_t n) { + int is_upload = (strncmp(rel, "/uploads/", 9) == 0); + if (is_upload) { + char head[SMALL_BUF]; + int hlen = snprintf(head, sizeof(head), + "HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: " + "%zu\r\nCache-Control: public, max-age=31536000, immutable\r\n\r\n", + mime, (size_t)n); + send(c, head, (size_t)hlen, 0); + if (n) send(c, buf, n, 0); + } else { + send_response(c, 200, "OK", mime, buf, n, false); + } +} +static void handle_static(int c, const char* path) { + char rel[SMALL_BUF]; + normalize_rel_path(path, rel, sizeof(rel)); + if (!safe_path(rel)) { + send_response(c, 403, "Forbidden", "text/plain", "Forbidden", 9, false); + return; + } + char full[SMALL_BUF * 2]; + snprintf(full, sizeof(full), "%s%s", DOC_ROOT ? DOC_ROOT : ".", rel); + char* buf = NULL; + size_t n = 0; + if (read_file_to_buf(full, &buf, &n) != 0) { + send_response(c, 404, "Not Found", "text/plain", "Not Found", 9, false); + return; + } + const char* mime = guess_mime(full); + send_cached_or_plain(c, rel, mime, buf, n); + free(buf); +} // removed unused read_headers_into -static void parse_request_line(const char* buf, char* method, char* path){ if(sscanf(buf, "%15s %4095s", method, path) < 2){ method[0]='\0'; path[0]='\0'; } } -static void parse_headers(const char* buf, size_t* content_length, char* ctype, size_t ctype_sz){ *content_length=0; ctype[0]='\0'; const char* cl=strcasestr(buf,"Content-Length:"); if(cl) *content_length=strtoul(cl+15,NULL,10); const char* ct=strcasestr(buf,"Content-Type:"); if(ct){ ct+=13; while(*ct==' '||*ct=='\t') ct++; size_t i=0; while(*ct && *ct!='\r' && *ct!='\n' && i header_bytes ? total - header_bytes : 0; char* body=NULL; if(content_length){ body=malloc(content_length+1); if(!body) return NULL; size_t off=0; if(have_body){ size_t cpy = have_body>content_length?content_length:have_body; memcpy(body, buf+header_bytes, cpy); off=cpy; } size_t remain=content_length-off; while(remain>0){ ssize_t rr=recv(c, body+off, remain, 0); if(rr<=0) break; off+=rr; remain-=rr; } body[content_length]='\0'; } return body; } -static void handle_client(int c){ char buf[RECV_BUF]; ssize_t total=0; // read headers - while(true){ ssize_t rcv=recv(c, buf+total, sizeof(buf)-1-total, 0); if(rcv<=0) break; total+=rcv; buf[total]='\0'; if(strstr(buf, "\r\n\r\n")) break; if(total >= (ssize_t)sizeof(buf)-1) break; } - if(total<=0){ close(c); return; } - char method[16]={0}, path[SMALL_BUF]={0}; parse_request_line(buf,method,path); if(!method[0]){ close(c); return; } - size_t content_length=0; char ctype[128]={0}; parse_headers(buf,&content_length,ctype,sizeof(ctype)); - char* body=read_body_if_needed(c,buf,(size_t)total,content_length); - if(!strncmp(path, "/api/", 5)) handle_api(c, method, path, body, content_length, ctype[0]?ctype:NULL); - else if(!strcmp(method,"GET")) handle_static(c, path); - else if(!strcmp(method,"OPTIONS")) send_response(c,204,"No Content","text/plain","",0,false); - else send_response(c,405,"Method Not Allowed","text/plain","",0,false); - free(body); close(c); +static void parse_request_line(const char* buf, char* method, char* path) { + if (sscanf(buf, "%15s %4095s", method, path) < 2) { + method[0] = '\0'; + path[0] = '\0'; + } +} +static void parse_headers(const char* buf, size_t* content_length, char* ctype, size_t ctype_sz) { + *content_length = 0; + ctype[0] = '\0'; + const char* cl = strcasestr(buf, "Content-Length:"); + if (cl) *content_length = strtoul(cl + 15, NULL, 10); + const char* ct = strcasestr(buf, "Content-Type:"); + if (ct) { + ct += 13; + while (*ct == ' ' || *ct == '\t') ct++; + size_t i = 0; + while (*ct && *ct != '\r' && *ct != '\n' && i < ctype_sz - 1) { + ctype[i++] = *ct++; + } + ctype[i] = '\0'; + } +} +// ---- small helpers to keep functions compact ---- +static size_t copy_from_buffer(char* dst, const char* src, size_t available, size_t max_n) { + size_t cpy = available > max_n ? max_n : available; + if (cpy) memcpy(dst, src, cpy); + return cpy; } -int main(int argc, char** argv){ - (void)argc; (void)argv; +static void recv_remaining_body(int c, char* body, size_t off, size_t total_len) { + size_t remain = total_len - off; + while (remain > 0) { + ssize_t rr = recv(c, body + off, remain, 0); + if (rr <= 0) break; + off += (size_t)rr; + remain -= (size_t)rr; + } +} +static char* read_body_if_needed(int c, const char* buf, size_t total, size_t content_length) { + if (!content_length) return NULL; + const char* hdr_end = strstr(buf, "\r\n\r\n"); + size_t header_bytes = hdr_end ? (size_t)(hdr_end - buf) + 4 : total; + size_t have_body = total > header_bytes ? total - header_bytes : 0; + char* body = (char*)malloc(content_length + 1); + if (!body) return NULL; + size_t off = 0; + if (have_body) off = copy_from_buffer(body, buf + header_bytes, have_body, content_length); + if (off < content_length) recv_remaining_body(c, body, off, content_length); + body[content_length] = '\0'; + return body; +} +static ssize_t read_request_headers(int c, char* buf, size_t bufsz) { + ssize_t total = 0; + while (true) { + ssize_t rcv = recv(c, buf + total, bufsz - 1 - (size_t)total, 0); + if (rcv <= 0) break; + total += rcv; + buf[total] = '\0'; + if (strstr(buf, "\r\n\r\n")) break; + if (total >= (ssize_t)bufsz - 1) break; + } + return total; +} + +static void process_request(int c, const char* buf, ssize_t total) { + char method[16] = {0}, path[SMALL_BUF] = {0}; + parse_request_line(buf, method, path); + if (!method[0]) return; + size_t content_length = 0; + char ctype[128] = {0}; + parse_headers(buf, &content_length, ctype, sizeof(ctype)); + char* body = read_body_if_needed(c, buf, (size_t)total, content_length); + if (!strncmp(path, "/api/", 5)) + handle_api(c, method, path, body, content_length, ctype[0] ? ctype : NULL); + else if (!strcmp(method, "GET")) handle_static(c, path); + else if (!strcmp(method, "OPTIONS")) + send_response(c, 204, "No Content", "text/plain", "", 0, false); + else send_response(c, 405, "Method Not Allowed", "text/plain", "", 0, false); + free(body); +} + +static void handle_client(int c) { + char buf[RECV_BUF]; + ssize_t total = read_request_headers(c, buf, sizeof(buf)); + if (total > 0) process_request(c, buf, total); + close(c); +} + +// NOLINTBEGIN(readability-function-size) +int main(int argc, char** argv) { + (void)argc; + (void)argv; signal(SIGINT, on_sigint); srand((unsigned int)time(NULL)); - char cwd[SMALL_BUF]; if(getcwd(cwd,sizeof(cwd))) DOC_ROOT=strdup(cwd); - const char* host = getenv_default("HOST","127.0.0.1"); - int port = atoi(getenv_default("PORT","8000")); if(port<=0) port=8000; + char cwd[SMALL_BUF]; + if (getcwd(cwd, sizeof(cwd))) DOC_ROOT = strdup(cwd); + const char* host = getenv_default("HOST", "127.0.0.1"); + int port = atoi(getenv_default("PORT", "8000")); + if (port <= 0) port = 8000; - int s = socket(AF_INET, SOCK_STREAM, 0); if(s<0){ perror("socket"); return 1; } - int opt=1; setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - struct sockaddr_in addr = {0}; addr.sin_family=AF_INET; addr.sin_port=htons((uint16_t)port); addr.sin_addr.s_addr=inet_addr(host); - if(bind(s,(struct sockaddr*)&addr,sizeof(addr))<0){ perror("bind"); close(s); return 1; } - if(listen(s,64)<0){ perror("listen"); close(s); return 1; } + int s = socket(AF_INET, SOCK_STREAM, 0); + if (s < 0) { + perror("socket"); + return 1; + } + int opt = 1; + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + addr.sin_addr.s_addr = inet_addr(host); + if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind"); + close(s); + return 1; + } + if (listen(s, 64) < 0) { + perror("listen"); + close(s); + return 1; + } printf("Serving Mini Articles (C) on http://%s:%d\n", host, port); - while(!g_stop){ struct sockaddr_in ca; socklen_t calen=sizeof(ca); int c=accept(s,(struct sockaddr*)&ca,&calen); if(c<0){ if(errno==EINTR) break; perror("accept"); continue; } handle_client(c); } + while (!g_stop) { + struct sockaddr_in ca; + socklen_t calen = sizeof(ca); + int c = accept(s, (struct sockaddr*)&ca, &calen); + if (c < 0) { + if (errno == EINTR) break; + perror("accept"); + continue; + } + handle_client(c); + } close(s); return 0; } +// NOLINTEND(readability-function-size) diff --git a/articles/tools/funcsize.awk b/articles/tools/funcsize.awk new file mode 100644 index 0000000..ab58c2c --- /dev/null +++ b/articles/tools/funcsize.awk @@ -0,0 +1,31 @@ +BEGIN{ in_func=0; depth=0; start=0; err=0; prev="" } +{ + line=$0 + # track function start when we see an opening brace at top-level and previous non-empty + # line looks like a function signature (ends with ')' and not ';', and not a typedef/struct/enum/union) + for(i=1;i<=length(line);i++){ + c=substr(line,i,1) + if(c=="{"){ + if(depth==0 && !in_func){ + # Heuristic check on previous non-empty trimmed line + t=prev + sub(/^\s+/, "", t); sub(/\s+$/, "", t) + if(t ~ /\)$/ && t !~ /;\s*$/ && t !~ /^(typedef|struct|enum|union)\b/){ + in_func=1; start=NR + } + } + depth++ + } else if(c=="}"){ + depth-- + if(in_func && depth==0){ + lines=NR-start+1 + if(lines>20){ print FILENAME ":" start " function too long: " lines " lines"; err=1 } + in_func=0 + } + } + } + # update previous non-empty line + tmp=line; sub(/^\s+/, "", tmp); sub(/\s+$/, "", tmp) + if(length(tmp)>0){ prev=tmp } +} +END{ if(err) exit 1 }