Mercurial
comparison markdown_converter/markdown_to_html.c @ 221:ce7f4400c2de hg-web
[hg-web] Harden forge and add changeset UI
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 02 Aug 2026 09:01:24 -0700 |
| parents | a2725419f988 |
| children |
comparison
equal
deleted
inserted
replaced
| 219:8c9bb0b0759e | 221:ce7f4400c2de |
|---|---|
| 59 buffer_grow(buf, len); | 59 buffer_grow(buf, len); |
| 60 memcpy(buf->data + buf->length, str, len + 1); | 60 memcpy(buf->data + buf->length, str, len + 1); |
| 61 buf->length += len; | 61 buf->length += len; |
| 62 } | 62 } |
| 63 | 63 |
| 64 static void buffer_append_n(StringBuffer *buf, const char *str, size_t n) | |
| 65 { | |
| 66 buffer_grow(buf, n); | |
| 67 memcpy(buf->data + buf->length, str, n); | |
| 68 buf->length += n; | |
| 69 buf->data[buf->length] = '\0'; | |
| 70 } | |
| 71 | |
| 72 static void buffer_append_char(StringBuffer *buf, char c) | 64 static void buffer_append_char(StringBuffer *buf, char c) |
| 73 { | 65 { |
| 74 buffer_grow(buf, 1); | 66 buffer_grow(buf, 1); |
| 75 buf->data[buf->length++] = c; | 67 buf->data[buf->length++] = c; |
| 76 buf->data[buf->length] = '\0'; | 68 buf->data[buf->length] = '\0'; |
| 69 } | |
| 70 | |
| 71 static void buffer_append_html_escaped_n(StringBuffer *buf, const char *text, size_t len) | |
| 72 { | |
| 73 for (size_t i = 0; i < len; i++) { | |
| 74 switch (text[i]) { | |
| 75 case '&': buffer_append(buf, "&"); break; | |
| 76 case '<': buffer_append(buf, "<"); break; | |
| 77 case '>': buffer_append(buf, ">"); break; | |
| 78 case '"': buffer_append(buf, """); break; | |
| 79 case '\'': buffer_append(buf, "'"); break; | |
| 80 default: buffer_append_char(buf, text[i]); break; | |
| 81 } | |
| 82 } | |
| 83 } | |
| 84 | |
| 85 static int is_safe_url(const char *url, size_t len, int is_image) | |
| 86 { | |
| 87 if (len == 0) return 0; | |
| 88 | |
| 89 for (size_t i = 0; i < len; i++) { | |
| 90 unsigned char c = (unsigned char)url[i]; | |
| 91 if (iscntrl(c) || isspace(c)) return 0; | |
| 92 } | |
| 93 | |
| 94 const char *colon = memchr(url, ':', len); | |
| 95 if (!colon) return 1; | |
| 96 | |
| 97 size_t scheme_len = (size_t)(colon - url); | |
| 98 if (scheme_len == 4 && strncasecmp(url, "http", scheme_len) == 0) return 1; | |
| 99 if (scheme_len == 5 && strncasecmp(url, "https", scheme_len) == 0) return 1; | |
| 100 if (!is_image && scheme_len == 6 && strncasecmp(url, "mailto", scheme_len) == 0) return 1; | |
| 101 return 0; | |
| 77 } | 102 } |
| 78 | 103 |
| 79 static void buffer_free(StringBuffer *buf) | 104 static void buffer_free(StringBuffer *buf) |
| 80 { | 105 { |
| 81 if (buf) { | 106 if (buf) { |
| 156 | 181 |
| 157 // Check for valid tag name (letter followed by alphanumeric) | 182 // Check for valid tag name (letter followed by alphanumeric) |
| 158 if (!isalpha((unsigned char)*line)) return 0; | 183 if (!isalpha((unsigned char)*line)) return 0; |
| 159 | 184 |
| 160 return 1; | 185 return 1; |
| 161 } | |
| 162 | |
| 163 // Check if line starts with a specific HTML tag (e.g., "script", "style") | |
| 164 static int is_html_tag(const char *line, const char *tag) | |
| 165 { | |
| 166 line = skip_whitespace(line); | |
| 167 if (*line != '<') return 0; | |
| 168 line++; | |
| 169 | |
| 170 // Skip optional / | |
| 171 int is_closing = 0; | |
| 172 if (*line == '/') { | |
| 173 is_closing = 1; | |
| 174 line++; | |
| 175 } | |
| 176 | |
| 177 size_t tag_len = strlen(tag); | |
| 178 if (strncasecmp(line, tag, tag_len) != 0) return 0; | |
| 179 | |
| 180 char next = line[tag_len]; | |
| 181 // Tag must be followed by space, >, or end for closing tags | |
| 182 return next == '>' || next == ' ' || next == '\t' || next == '\n' || next == '\0'; | |
| 183 } | 186 } |
| 184 | 187 |
| 185 // Check if line is ordered list item | 188 // Check if line is ordered list item |
| 186 static int is_ordered_list(const char *line) | 189 static int is_ordered_list(const char *line) |
| 187 { | 190 { |
| 357 size_t url_start = link_end + 2; | 360 size_t url_start = link_end + 2; |
| 358 size_t url_end = url_start; | 361 size_t url_end = url_start; |
| 359 while (url_end < len && text[url_end] != ')') url_end++; | 362 while (url_end < len && text[url_end] != ')') url_end++; |
| 360 | 363 |
| 361 if (url_end < len) { | 364 if (url_end < len) { |
| 362 buffer_append(buf, "<a href=\""); | 365 size_t url_len = url_end - url_start; |
| 363 buffer_append_n(buf, text + url_start, url_end - url_start); | 366 if (is_safe_url(text + url_start, url_len, 0)) { |
| 364 buffer_append(buf, "\">"); | 367 buffer_append(buf, "<a href=\""); |
| 365 buffer_append_n(buf, text + link_start, link_end - link_start); | 368 buffer_append_html_escaped_n(buf, text + url_start, url_len); |
| 366 buffer_append(buf, "</a>"); | 369 buffer_append(buf, "\">"); |
| 370 process_inline(buf, text + link_start, link_end - link_start); | |
| 371 buffer_append(buf, "</a>"); | |
| 372 } else { | |
| 373 process_inline(buf, text + link_start, link_end - link_start); | |
| 374 } | |
| 367 i = url_end + 1; | 375 i = url_end + 1; |
| 368 continue; | 376 continue; |
| 369 } | 377 } |
| 370 } | 378 } |
| 371 } | 379 } |
| 380 size_t url_start = alt_end + 2; | 388 size_t url_start = alt_end + 2; |
| 381 size_t url_end = url_start; | 389 size_t url_end = url_start; |
| 382 while (url_end < len && text[url_end] != ')') url_end++; | 390 while (url_end < len && text[url_end] != ')') url_end++; |
| 383 | 391 |
| 384 if (url_end < len) { | 392 if (url_end < len) { |
| 385 buffer_append(buf, "<img src=\""); | 393 size_t url_len = url_end - url_start; |
| 386 buffer_append_n(buf, text + url_start, url_end - url_start); | 394 if (is_safe_url(text + url_start, url_len, 1)) { |
| 387 buffer_append(buf, "\" alt=\""); | 395 buffer_append(buf, "<img src=\""); |
| 388 buffer_append_n(buf, text + alt_start, alt_end - alt_start); | 396 buffer_append_html_escaped_n(buf, text + url_start, url_len); |
| 389 buffer_append(buf, "\">"); | 397 buffer_append(buf, "\" alt=\""); |
| 398 buffer_append_html_escaped_n(buf, text + alt_start, alt_end - alt_start); | |
| 399 buffer_append(buf, "\">"); | |
| 400 } else { | |
| 401 buffer_append_html_escaped_n(buf, text + alt_start, alt_end - alt_start); | |
| 402 } | |
| 390 i = url_end + 1; | 403 i = url_end + 1; |
| 391 continue; | 404 continue; |
| 392 } | 405 } |
| 393 } | 406 } |
| 394 } | 407 } |
| 447 size_t end = start; | 460 size_t end = start; |
| 448 while (end < len && text[end] != '`') end++; | 461 while (end < len && text[end] != '`') end++; |
| 449 | 462 |
| 450 if (end < len) { | 463 if (end < len) { |
| 451 buffer_append(buf, "<code>"); | 464 buffer_append(buf, "<code>"); |
| 452 buffer_append_n(buf, text + start, end - start); | 465 buffer_append_html_escaped_n(buf, text + start, end - start); |
| 453 buffer_append(buf, "</code>"); | 466 buffer_append(buf, "</code>"); |
| 454 i = end + 1; | 467 i = end + 1; |
| 455 continue; | 468 continue; |
| 456 } | 469 } |
| 457 } | 470 } |
| 458 | 471 |
| 459 // This might not be needed for now. | 472 buffer_append_html_escaped_n(buf, text + i, 1); |
| 460 // HTML escape special characters | |
| 461 // if (text[i] == '<') { | |
| 462 // buffer_append(buf, "<"); | |
| 463 // } else if (text[i] == '>') { | |
| 464 // buffer_append(buf, ">"); | |
| 465 // } else if (text[i] == '&') { | |
| 466 // buffer_append(buf, "&"); | |
| 467 // } else { | |
| 468 // buffer_append_char(buf, text[i]); | |
| 469 // } | |
| 470 buffer_append_char(buf, text[i]); | |
| 471 i++; | 473 i++; |
| 472 } | 474 } |
| 473 } | 475 } |
| 474 | 476 |
| 475 // Convert markdown to HTML | 477 // Convert markdown to HTML |
| 757 } | 759 } |
| 758 free(next_line); | 760 free(next_line); |
| 759 } | 761 } |
| 760 } | 762 } |
| 761 | 763 |
| 762 // HTML block - pass through unchanged | 764 // Repository markdown is untrusted. Render raw HTML as text. |
| 763 if (is_html_block_start(line)) { | 765 if (is_html_block_start(line)) { |
| 764 // Check if it's a script or style tag that needs special handling | 766 buffer_append(buf, "<p>"); |
| 765 int is_script = is_html_tag(line, "script"); | 767 process_inline(buf, line, line_len); |
| 766 int is_style = is_html_tag(line, "style"); | 768 buffer_append(buf, "</p>"); |
| 767 | |
| 768 if (is_script || is_style) { | |
| 769 const char *end_tag = is_script ? "</script>" : "</style>"; | |
| 770 | |
| 771 // Output the opening line | |
| 772 buffer_append(buf, line); | |
| 773 buffer_append_char(buf, '\n'); | |
| 774 | |
| 775 free(line); | |
| 776 if (*ptr == '\n') ptr++; | |
| 777 | |
| 778 // Collect content until closing tag | |
| 779 while (*ptr) { | |
| 780 line_start = ptr; | |
| 781 while (*ptr && *ptr != '\n') ptr++; | |
| 782 line_len = ptr - line_start; | |
| 783 | |
| 784 line = (char *)malloc(line_len + 1); | |
| 785 if (!line) break; | |
| 786 memcpy(line, line_start, line_len); | |
| 787 line[line_len] = '\0'; | |
| 788 | |
| 789 buffer_append(buf, line); | |
| 790 buffer_append_char(buf, '\n'); | |
| 791 | |
| 792 int found_end = (strstr(line, end_tag) != NULL); | |
| 793 free(line); | |
| 794 if (*ptr == '\n') ptr++; | |
| 795 | |
| 796 if (found_end) break; | |
| 797 } | |
| 798 continue; | |
| 799 } | |
| 800 | |
| 801 // Regular HTML tag - just pass through the line | |
| 802 buffer_append(buf, line); | |
| 803 buffer_append_char(buf, '\n'); | |
| 804 free(line); | 769 free(line); |
| 805 if (*ptr == '\n') ptr++; | 770 if (*ptr == '\n') ptr++; |
| 806 continue; | 771 continue; |
| 807 } | 772 } |
| 808 | 773 |