├── .gitmodules ├── public ├── favicon.png ├── reset.css ├── index.html ├── main.css └── index.js ├── .gitignore ├── default.nix ├── src ├── error_page_template.h.tt ├── utf8.h ├── response.h ├── request.h ├── schedule.h ├── json_check.c ├── memory.h ├── tt.c ├── utf8.c ├── platform_specific.h ├── json.h ├── s.h ├── json_test.c ├── schedule.c ├── main.c └── json.c ├── .github └── workflows │ └── ci.yml ├── Makefile ├── README.md ├── LICENSE └── schedule.json /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/skedudle/HEAD/public/favicon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # binaries 2 | skedudle 3 | tt 4 | *_test 5 | json_check 6 | 7 | # intermediate garbage 8 | *_template.h 9 | *.o 10 | 11 | # vim 12 | *.swp 13 | 14 | *.gcda 15 | *.gcno 16 | *.gcov 17 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | with import {}; rec { 2 | skedudleEnv = stdenv.mkDerivation { 3 | name = "skedudle-env"; 4 | buildInputs = [ stdenv gcc gdb valgrind gnumake pkgconfig ]; 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /src/error_page_template.h.tt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Error code %INT(code)% 5 | 6 | 7 |

Error code %INT(code)%

8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build-linux-gcc: 6 | runs-on: ubuntu-18.04 7 | steps: 8 | - uses: actions/checkout@v1 9 | - name: build 10 | run: make 11 | -------------------------------------------------------------------------------- /src/utf8.h: -------------------------------------------------------------------------------- 1 | #ifndef UTF8_H_ 2 | #define UTF8_H_ 3 | 4 | #include 5 | 6 | #define UTF8_CHUNK_CAPACITY 4 7 | 8 | typedef struct { 9 | size_t size; 10 | uint8_t buffer[UTF8_CHUNK_CAPACITY]; 11 | } Utf8_Chunk; 12 | 13 | Utf8_Chunk utf8_encode_rune(uint32_t rune); 14 | 15 | #endif // UTF8_H_ 16 | -------------------------------------------------------------------------------- /src/response.h: -------------------------------------------------------------------------------- 1 | #ifndef RESPONSE_H_ 2 | #define RESPONSE_H_ 3 | 4 | void response_status_line(int fd, int code) 5 | { 6 | dprintf(fd, "HTTP/1.1 %d\n", code); 7 | } 8 | 9 | void response_header(int fd, const char *name, const char *value_format, ...) 10 | { 11 | va_list args; 12 | va_start(args, value_format); 13 | 14 | dprintf(fd, "%s: ", name); 15 | vdprintf(fd, value_format, args); 16 | dprintf(fd, "\n"); 17 | 18 | va_end(args); 19 | } 20 | 21 | void response_body_start(int fd) 22 | { 23 | dprintf(fd, "\n"); 24 | } 25 | 26 | #endif // RESPONSE_H_ 27 | -------------------------------------------------------------------------------- /src/request.h: -------------------------------------------------------------------------------- 1 | #ifndef REQUEST_H_ 2 | #define REQUEST_H_ 3 | 4 | typedef struct { 5 | String method; 6 | String path; 7 | } Status_Line; 8 | 9 | Status_Line chop_status_line(String *buffer) 10 | { 11 | String line = trim_end(chop_line(buffer)); 12 | Status_Line result; 13 | result.method = chop_word(&line); 14 | result.path = chop_word(&line); 15 | return result; 16 | } 17 | 18 | typedef struct { 19 | String name; 20 | String value; 21 | } Header; 22 | 23 | Header parse_header(String line) 24 | { 25 | Header result; 26 | result.name = trim(chop_until_char(&line, ':')); 27 | result.value = trim(line); 28 | return result; 29 | } 30 | 31 | #endif // REQUEST_H_ 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS=-Wall -Wextra -Wno-unused-result -pedantic -std=c11 -ggdb 2 | CS=src/main.c src/schedule.c src/json.c src/utf8.c 3 | HS=src/s.h src/request.h src/response.h src/error_page_template.h src/schedule.h src/json.h src/platform_specific.h 4 | LIBS=-lm 5 | 6 | all: skedudle json_test json_check 7 | 8 | skedudle: $(CS) $(HS) 9 | $(CC) $(CFLAGS) -o skedudle $(CS) $(LIBS) 10 | 11 | tt: src/tt.c 12 | $(CC) $(CFLAGS) -o tt src/tt.c 13 | 14 | src/error_page_template.h: tt src/error_page_template.h.tt 15 | ./tt src/error_page_template.h.tt > src/error_page_template.h 16 | 17 | json_test: src/json.c src/json_test.c src/s.h src/memory.h src/utf8.h src/utf8.c 18 | $(CC) $(CFLAGS) -o json_test src/json.c src/json_test.c src/utf8.c $(LIBS) 19 | 20 | json_check: src/json.c src/json_check.c src/s.h src/memory.h src/utf8.h src/utf8.c 21 | $(CC) $(CFLAGS) -o json_check src/json.c src/json_check.c src/utf8.c $(LIBS) 22 | -------------------------------------------------------------------------------- /src/schedule.h: -------------------------------------------------------------------------------- 1 | #ifndef SCHEDULE_H_ 2 | #define SCHEDULE_H_ 3 | 4 | #include 5 | #include 6 | #include "s.h" 7 | #include "memory.h" 8 | #include "json.h" 9 | 10 | struct Project 11 | { 12 | String name; 13 | String description; 14 | String url; 15 | uint8_t days; 16 | int time_min; 17 | String channel; 18 | struct tm *starts; 19 | struct tm *ends; 20 | }; 21 | 22 | struct Event 23 | { 24 | struct tm date; 25 | int time_min; 26 | String title; 27 | String description; 28 | String url; 29 | String channel; 30 | }; 31 | 32 | struct Schedule 33 | { 34 | struct Project *projects; 35 | size_t projects_size; 36 | time_t *cancelled_events; 37 | size_t cancelled_events_count; 38 | struct Event *extra_events; 39 | size_t extra_events_size; 40 | String timezone; 41 | }; 42 | 43 | struct Schedule json_as_schedule(Memory *memory, Json_Value input); 44 | 45 | #endif // SCHEDULE_H_ 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Tsoding](https://img.shields.io/badge/twitch.tv-tsoding-purple?logo=twitch&style=for-the-badge)](https://www.twitch.tv/tsoding) 2 | 3 | # Skedudle 4 | 5 | Simple Event Schedule Web Application in C. 6 | 7 | **WARNING! The application is in an active development state and is 8 | not even alpha yet. Use it at your own risk. Nothing is documented, 9 | anything can be changed at any moment or stop working at all.** 10 | 11 | ## Goals 12 | 13 | This project has two goals: 14 | 1. Implement backend for https://github.com/tsoding/schedule 15 | 2. Implement enough Web related code to later extract it as an epic Web Framework in C 16 | 17 | ## Quick Start 18 | 19 | ```console 20 | $ make 21 | $ ./skedudle ./schedule.json 6969 22 | $ http://localhost:6969 23 | ``` 24 | 25 | ## Support 26 | 27 | You can support my work via 28 | 29 | - Twitch channel: https://www.twitch.tv/subs/tsoding 30 | - Patreon: https://www.patreon.com/tsoding 31 | 32 | ## References 33 | 34 | - https://github.com/stedolan/jq/blob/9b51a0852a0f91fbc987f5f2b302ff65e22f6399/src/jv_parse.c#L455 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Skedudle Developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /src/json_check.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "json.h" 12 | 13 | #define MEMORY_CAPACITY (10 * 1000 * 1000) 14 | 15 | static uint8_t memory_buffer[MEMORY_CAPACITY]; 16 | static Memory memory = { 17 | .capacity = MEMORY_CAPACITY, 18 | .buffer = memory_buffer, 19 | }; 20 | 21 | String mmap_file_to_string(const char *filepath) 22 | { 23 | int fd = open(filepath, O_RDONLY); 24 | assert(fd >= 0); 25 | 26 | struct stat fd_stat; 27 | int err = fstat(fd, &fd_stat); 28 | assert(err == 0); 29 | 30 | String result; 31 | result.len = fd_stat.st_size; 32 | result.data = mmap(NULL, result.len, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); 33 | assert(result.data != MAP_FAILED); 34 | close(fd); 35 | 36 | return result; 37 | } 38 | 39 | int main(int argc, char *argv[]) 40 | { 41 | assert(argc >= 2); 42 | String file_content = mmap_file_to_string(argv[1]); 43 | Json_Result result = parse_json_value(&memory, file_content); 44 | return result.is_error || trim_begin(result.rest).len > 0; 45 | } 46 | -------------------------------------------------------------------------------- /public/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /src/memory.h: -------------------------------------------------------------------------------- 1 | #ifndef MEMORY_H_ 2 | #define MEMORY_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #define KILO 1024 9 | #define MEGA (1024 * KILO) 10 | #define GIGA (1024 * MEGA) 11 | 12 | typedef struct { 13 | size_t capacity; 14 | size_t size; 15 | uint8_t *buffer; 16 | } Memory; 17 | 18 | static inline 19 | void *memory_alloc(Memory *memory, size_t size) 20 | { 21 | assert(memory); 22 | assert(memory->size + size <= memory->capacity); 23 | 24 | 25 | void *result = memory->buffer + memory->size; 26 | memory->size += size; 27 | 28 | return result; 29 | } 30 | 31 | static inline 32 | void *memory_alloc_aligned(Memory *memory, size_t size, size_t alignment) 33 | { 34 | assert(memory); 35 | 36 | // this gets aligns the address *upwards*, to the next alignment. 37 | // the assumption here is that 'alignment' is a power of two. 38 | assert((alignment & (alignment - 1)) == 0); 39 | 40 | uintptr_t ptr = (uintptr_t) (memory->buffer + memory->size + (alignment - 1)); 41 | uint8_t *result = (uint8_t *) (ptr & ~(alignment - 1)); 42 | 43 | // since result and buffer are uint8_t*, this gives us bytes. 44 | size_t real_size = (result + size) - (memory->buffer + memory->size); 45 | assert(memory->size + real_size <= memory->capacity); 46 | 47 | memory->size += real_size; 48 | return result; 49 | } 50 | 51 | static inline 52 | void memory_clean(Memory *memory) 53 | { 54 | assert(memory); 55 | memory->size = 0; 56 | } 57 | 58 | #endif // MEMORY_H_ 59 | -------------------------------------------------------------------------------- /src/tt.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "s.h" 7 | 8 | String file_as_content(const char *filepath) { 9 | assert(filepath); 10 | 11 | FILE *f = fopen(filepath, "rb"); 12 | assert(f); 13 | 14 | fseek(f, 0, SEEK_END); 15 | long m = ftell(f); 16 | assert(m >= 0); 17 | fseek(f, 0, SEEK_SET); 18 | char *buffer = calloc(1, sizeof(char) * (size_t) m + 1); 19 | assert(buffer); 20 | 21 | size_t n = fread(buffer, 1, (size_t) m, f); 22 | assert(n == (size_t) m); 23 | 24 | fclose(f); 25 | return string(n, buffer); 26 | } 27 | 28 | void compile_c_code(String s) { 29 | printf("%.*s\n", (int) s.len, s.data); 30 | } 31 | 32 | void compile_byte_array(String s) { 33 | printf("write(OUT, \""); 34 | for (uint64_t i = 0; i < s.len; ++i) { 35 | printf("\\x%02x", s.data[i]); 36 | } 37 | printf("\", %lu);\n", s.len); 38 | } 39 | 40 | int main(int argc, char *argv[]) 41 | { 42 | if (argc < 2) { 43 | fprintf(stderr, "Usage: ./tt \n"); 44 | return 1; 45 | } 46 | const char *filepath = argv[1]; 47 | String template = file_as_content(filepath); 48 | int c_code_mode = 0; 49 | while (template.len) { 50 | String token = chop_until_char(&template, '%'); 51 | if (c_code_mode) { 52 | compile_c_code(token); 53 | } else { 54 | compile_byte_array(token); 55 | } 56 | c_code_mode = !c_code_mode; 57 | } 58 | 59 | return 0; 60 | } 61 | -------------------------------------------------------------------------------- /src/utf8.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "utf8.h" 4 | 5 | Utf8_Chunk utf8_encode_rune(uint32_t rune) 6 | { 7 | const uint8_t b00000111 = (1 << 3) - 1; 8 | const uint8_t b00001111 = (1 << 4) - 1; 9 | const uint8_t b00011111 = (1 << 5) - 1; 10 | const uint8_t b00111111 = (1 << 6) - 1; 11 | const uint8_t b10000000 = ~((1 << 7) - 1); 12 | const uint8_t b11000000 = ~((1 << 6) - 1); 13 | const uint8_t b11100000 = ~((1 << 5) - 1); 14 | const uint8_t b11110000 = ~((1 << 4) - 1); 15 | 16 | if (rune <= 0x007F) { 17 | return (Utf8_Chunk) { 18 | .size = 1, 19 | .buffer = {rune} 20 | }; 21 | } else if (0x0080 <= rune && rune <= 0x07FF) { 22 | return (Utf8_Chunk) { 23 | .size = 2, 24 | .buffer = {((rune >> 6) & b00011111) | b11000000, 25 | (rune & b00111111) | b10000000} 26 | }; 27 | } else if (0x0800 <= rune && rune <= 0xFFFF) { 28 | return (Utf8_Chunk){ 29 | .size = 3, 30 | .buffer = {((rune >> 12) & b00001111) | b11100000, 31 | ((rune >> 6) & b00111111) | b10000000, 32 | (rune & b00111111) | b10000000} 33 | }; 34 | } else if (0x10000 <= rune && rune <= 0x10FFFF) { 35 | return (Utf8_Chunk){ 36 | .size = 4, 37 | .buffer = {((rune >> 18) & b00000111) | b11110000, 38 | ((rune >> 12) & b00111111) | b10000000, 39 | ((rune >> 6) & b00111111) | b10000000, 40 | (rune & b00111111) | b10000000} 41 | }; 42 | } else { 43 | return (Utf8_Chunk){0}; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Schedule — Tsoding 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Schedule

13 |
for twitch.tv/tsoding streams
14 |
15 | 16 |
17 | 18 | 19 |
20 | © 2020 Tsoding 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /public/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #181818; 3 | color: #e4e4ef; 4 | width: 720px; 5 | margin-left: auto; 6 | margin-right: auto; 7 | font-family: "Cantarell", sans-serif; 8 | } 9 | 10 | .event { 11 | padding: 20px; 12 | border: 1px solid #515151; 13 | margin-top: 10px; 14 | text-align: center; 15 | background: #1b1b1b; 16 | } 17 | .event h1 { 18 | font-size: 25px; 19 | margin-bottom: 10px; 20 | width: 540px; 21 | margin-left: auto; 22 | margin-right: auto; 23 | text-align: center; 24 | } 25 | .event .countdown { 26 | font-size: 20px; 27 | margin-bottom: 10px; 28 | } 29 | .event .channel { 30 | font-size: 15px; 31 | } 32 | .event .timestamp { 33 | position: relative; 34 | color: #515151; 35 | font-size: 11px; 36 | height: 0px; 37 | text-align: left; 38 | } 39 | .event .timestamp a { 40 | color: #515151; 41 | } 42 | .event .watch { 43 | height: 0px; 44 | text-align: right; 45 | color: #f43841; 46 | } 47 | .event .description { 48 | margin-top: 20px; 49 | text-align: left; 50 | color: #848484; 51 | line-height: 20px; 52 | width: 540px; 53 | margin-left: auto; 54 | margin-right: auto; 55 | } 56 | .event .description a { 57 | color: #68b531; 58 | } 59 | .event .description ul { 60 | padding-top: 10px; 61 | } 62 | .event .description ul li { 63 | margin-left: 20px; 64 | list-style-type: circle; 65 | } 66 | .event .description ol { 67 | padding-top: 10px; 68 | } 69 | .event .description ol li { 70 | margin-left: 20px; 71 | list-style-type: decimal; 72 | } 73 | .event .player { 74 | padding-top: 20px; 75 | width: 540px; 76 | height: 303.75px; 77 | } 78 | 79 | .event.past { 80 | color: #515151; 81 | background: #181818; 82 | } 83 | .event.past a { 84 | color: #515151; 85 | } 86 | .event.past .description { 87 | color: #515151; 88 | } 89 | .event.past .cancelled-stamp { 90 | position: relative; 91 | height: 0px; 92 | color: #f43841; 93 | font-size: 50px; 94 | transform: rotate(20deg); 95 | top: -80px; 96 | } 97 | 98 | .event.current { 99 | border: 3px solid #f43841; 100 | } 101 | 102 | a { 103 | font-kerning: normal; 104 | font-family: "Cantarell", sans-serif; 105 | color: #73c936; 106 | text-decoration: none; 107 | } 108 | 109 | strong { 110 | font-weight: bold; 111 | } 112 | 113 | .header { 114 | padding: 30px; 115 | text-align: center; 116 | } 117 | .header h1 { 118 | font-size: 60px; 119 | } 120 | .header .subheader a { 121 | font-size: 15px; 122 | border-bottom: 1px solid; 123 | } 124 | 125 | .markdown p { 126 | margin-top: 10px; 127 | } 128 | .markdown a { 129 | border-bottom: 1px solid; 130 | } 131 | 132 | footer { 133 | padding-top: 30px; 134 | padding-bottom: 30px; 135 | margin-top: 20px; 136 | } 137 | -------------------------------------------------------------------------------- /src/platform_specific.h: -------------------------------------------------------------------------------- 1 | #ifndef PLATFORM_SPECIFIC_H_ 2 | #define PLATFORM_SPECIFIC_H_ 3 | 4 | // this is quite lame because there is no one definition for all BSDs 5 | // and i don't really wanna assume !linux == BSD 6 | 7 | #if __linux__ 8 | #include 9 | 10 | static inline 11 | ssize_t sendfile_wrapper(int out_fd, int in_fd, off_t* offset, size_t count) 12 | { 13 | return sendfile(out_fd, in_fd, offset, count); 14 | } 15 | 16 | #elif (__FreeBSD__ || __NetBSD__ || __OpenBSD__ || __DragonFly__) 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | static inline 25 | ssize_t sendfile_wrapper(int out_fd, int in_fd, off_t* offset, size_t count) 26 | { 27 | // bsd signature is: 28 | // sendfile(int fd, int sock, off_t offset, size_t nbytes, struct sf_hdtr*, off_t* sbytes, int flags) 29 | 30 | // we need to do this, because the behaviour of linux sendfile is to read from the 31 | // current seek location in the file if the offset pointer is NULL. BSD sendfile presumably 32 | // always uses the offset, and it does not seek in the file after sending either. 33 | off_t file_offset = 0; 34 | 35 | if(offset != NULL) 36 | file_offset = *offset; 37 | 38 | else 39 | file_offset = lseek(in_fd, 0, SEEK_CUR); 40 | 41 | off_t sent_bytes = 0; 42 | int result = sendfile(in_fd, out_fd, file_offset, count, NULL, &sent_bytes, 0); 43 | 44 | if(result < 0) 45 | return (ssize_t) result; 46 | 47 | // if the offset pointer was null, then update the file cursor 48 | // if not, then update the user-given offset pointer. 49 | if(offset == NULL) 50 | lseek(in_fd, sent_bytes, SEEK_CUR); 51 | 52 | else 53 | *offset += sent_bytes; 54 | 55 | return (ssize_t) (file_offset + sent_bytes); 56 | } 57 | 58 | #elif (__APPLE__ && __MACH__) 59 | 60 | 61 | #include 62 | #include 63 | #include 64 | #include 65 | 66 | // see above for most of the comments. the OSX version is a bit more complicated 67 | // because the number of bytes is a "value-result parameter". 68 | 69 | static inline 70 | ssize_t sendfile_wrapper(int out_fd, int in_fd, off_t* offset, size_t count) 71 | { 72 | // osx signature is: 73 | // sendfile(int fd, int sock, off_t offset, off_t* len, struct sf_hdtr*, int flags) 74 | off_t file_offset = 0; 75 | 76 | if(offset != NULL) 77 | file_offset = *offset; 78 | 79 | else 80 | file_offset = lseek(in_fd, 0, SEEK_CUR); 81 | 82 | // we set this, because the third parameter is both the (input) number of bytes to send, 83 | // and the (output) actual number of bytes sent. 84 | off_t sent_bytes = count; 85 | int result = sendfile(in_fd, out_fd, file_offset, &sent_bytes, NULL, 0); 86 | 87 | if(result < 0) 88 | return (ssize_t) result; 89 | 90 | // if the offset pointer was null, then update the file cursor 91 | // if not, then update the user-given offset pointer. 92 | if(offset == NULL) 93 | lseek(in_fd, sent_bytes, SEEK_CUR); 94 | 95 | else 96 | *offset += sent_bytes; 97 | 98 | return (ssize_t) (file_offset + sent_bytes); 99 | } 100 | 101 | 102 | #endif 103 | 104 | #endif // PLATFORM_SPECIFIC_H_ 105 | -------------------------------------------------------------------------------- /src/json.h: -------------------------------------------------------------------------------- 1 | #ifndef JSON_H_ 2 | #define JSON_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include "s.h" 8 | #include "memory.h" 9 | 10 | #define JSON_DEPTH_MAX_LIMIT 100 11 | 12 | typedef enum { 13 | JSON_NULL = 0, 14 | JSON_BOOLEAN, 15 | JSON_NUMBER, 16 | JSON_STRING, 17 | JSON_ARRAY, 18 | JSON_OBJECT 19 | } Json_Type; 20 | 21 | static inline 22 | const char *json_type_as_cstr(Json_Type type) 23 | { 24 | switch (type) { 25 | case JSON_NULL: return "JSON_NULL"; 26 | case JSON_BOOLEAN: return "JSON_BOOLEAN"; 27 | case JSON_NUMBER: return "JSON_NUMBER"; 28 | case JSON_STRING: return "JSON_STRING"; 29 | case JSON_ARRAY: return "JSON_ARRAY"; 30 | case JSON_OBJECT: return "JSON_OBJECT"; 31 | } 32 | 33 | assert(!"Incorrect Json_Type"); 34 | } 35 | 36 | typedef struct Json_Value Json_Value; 37 | 38 | #define JSON_ARRAY_PAGE_CAPACITY 128 39 | 40 | typedef struct Json_Array_Page Json_Array_Page; 41 | 42 | typedef struct { 43 | Json_Array_Page *begin; 44 | Json_Array_Page *end; 45 | } Json_Array; 46 | 47 | void json_array_push(Memory *memory, Json_Array *array, Json_Value value); 48 | 49 | typedef struct Json_Object_Page Json_Object_Page; 50 | 51 | typedef struct { 52 | Json_Object_Page *begin; 53 | Json_Object_Page *end; 54 | } Json_Object; 55 | 56 | typedef struct { 57 | // TODO(#26): because of the use of String-s Json_Number can hold an incorrect value 58 | // But you can only get an incorrect Json_Number if you construct it yourself. 59 | // Anything coming from parse_json_value should be always a correct number. 60 | String integer; 61 | String fraction; 62 | String exponent; 63 | } Json_Number; 64 | 65 | int64_t json_number_to_integer(Json_Number number); 66 | 67 | struct Json_Value { 68 | Json_Type type; 69 | union 70 | { 71 | int boolean; 72 | Json_Number number; 73 | String string; 74 | Json_Array array; 75 | Json_Object object; 76 | }; 77 | }; 78 | 79 | struct Json_Array_Page { 80 | Json_Array_Page *next; 81 | size_t size; 82 | Json_Value elements[JSON_ARRAY_PAGE_CAPACITY]; 83 | }; 84 | 85 | static inline 86 | size_t json_array_size(Json_Array array) 87 | { 88 | size_t size = 0; 89 | for (Json_Array_Page *page = array.begin; 90 | page != NULL; 91 | page = page->next) 92 | { 93 | size += page->size; 94 | } 95 | return size; 96 | } 97 | 98 | typedef struct { 99 | Json_Value value; 100 | String rest; 101 | int is_error; 102 | const char *message; 103 | } Json_Result; 104 | 105 | typedef struct { 106 | String key; 107 | Json_Value value; 108 | } Json_Object_Member; 109 | 110 | #define JSON_OBJECT_PAGE_CAPACITY 128 111 | 112 | extern Json_Value json_null; 113 | extern Json_Value json_true; 114 | extern Json_Value json_false; 115 | 116 | Json_Value json_string(String string); 117 | 118 | struct Json_Object_Page { 119 | Json_Object_Page *next; 120 | size_t size; 121 | Json_Object_Member elements[JSON_OBJECT_PAGE_CAPACITY]; 122 | }; 123 | 124 | void json_object_push(Memory *memory, Json_Object *object, String key, Json_Value value); 125 | 126 | // TODO(#40): parse_json_value is not aware of input encoding 127 | Json_Result parse_json_value(Memory *memory, String source); 128 | void print_json_error(FILE *stream, Json_Result result, String source, const char *prefix); 129 | void print_json_value(FILE *stream, Json_Value value); 130 | void print_json_value_fd(int fd, Json_Value value); 131 | 132 | #endif // JSON_H_ 133 | -------------------------------------------------------------------------------- /public/index.js: -------------------------------------------------------------------------------- 1 | function humanReadableTimeDiff(diff = 0) { 2 | return [ 3 | ["day", Math.floor(Math.abs(diff) / 60 / 60 / 24)], 4 | ["hour", Math.floor(Math.abs(diff) / 60 / 60 % 24)], 5 | ["minute", Math.floor(Math.abs(diff) / 60 % 60)], 6 | ["second", Math.floor(Math.abs(diff) % 60)] 7 | ].filter( 8 | ([_, value]) => value > 0 9 | ).map( 10 | ([name, value]) => `${value} ${value == 1 ? name : name + "s"}` 11 | ).join(' '); 12 | } 13 | 14 | function createTag(name, attributes = {}, innerText = "") 15 | { 16 | let tag = document.createElement(name); 17 | for (let [key, value] of Object.entries(attributes)) { 18 | tag.setAttribute(key, value); 19 | } 20 | tag.innerText = innerText; 21 | return tag; 22 | } 23 | 24 | function createTimestamp(json) 25 | { 26 | let timestamp = createTag("div", {"class": "timestamp"}); 27 | timestamp.innerHTML = `${json["id"]}`; 28 | return timestamp; 29 | } 30 | 31 | function createTitle(json) 32 | { 33 | let title = createTag("h1"); 34 | title.innerHTML = `${json["title"]}`; 35 | return title; 36 | } 37 | 38 | function timeDiffOfEvent(event) 39 | { 40 | return parseInt(event["id"]) - Math.floor(Date.now() / 1000); 41 | } 42 | 43 | function createCountdown(event) 44 | { 45 | let countdown = createTag("div", {"class": "countdown"}); 46 | let diff = timeDiffOfEvent(event); 47 | 48 | if (diff >= 0) { 49 | countdown.innerHTML = `Starts in ${humanReadableTimeDiff(diff)}`; 50 | } else { 51 | countdown.innerHTML = `Ended ${humanReadableTimeDiff(diff)} ago`; 52 | } 53 | 54 | return countdown; 55 | } 56 | 57 | function createChannel(json) { 58 | let channel = createTag("div", {"class": "channel"}); 59 | channel.innerHTML = `${json["channel"]}`; 60 | return channel; 61 | } 62 | 63 | function createDescription(json) { 64 | let desc = createTag("div", {"class": "description markdown"}); 65 | desc.innerHTML = json["description"]; 66 | return desc; 67 | } 68 | 69 | function createDayOff() { 70 | let dayOff = createTag("div", {"class": "event"}); 71 | dayOff.appendChild(createTag("h1", {}, "Day off")); 72 | return dayOff; 73 | } 74 | 75 | function createEvent(event) { 76 | if (event) { 77 | let eventTag = createTag("div", { 78 | "class": timeDiffOfEvent(event) < 0 ? "event past" : "event", 79 | "id": "_" + event["id"] 80 | }); 81 | 82 | // TODO(#63): the frontend does not display a couple of past events like the legacy app 83 | // TODO(#64): the frontend does not display the current event with embeded twitch stream 84 | eventTag.appendChild(createTimestamp(event)); 85 | eventTag.appendChild(createTitle(event)); 86 | eventTag.appendChild(createCountdown(event)); 87 | eventTag.appendChild(createChannel(event)); 88 | // TODO(#65): markdown in the description is not renderered; 89 | eventTag.appendChild(createDescription(event)); 90 | 91 | return eventTag; 92 | } 93 | 94 | return createDayOff(); 95 | } 96 | 97 | (() => { 98 | let app = document.querySelector("#app"); 99 | fetch("/api/period_streams") 100 | .then(res => res.json()) 101 | .then(json => { 102 | for (let event in json) { 103 | app.appendChild(createEvent(json[event])); 104 | } 105 | }) 106 | .catch(err => { 107 | app.innerText = "Bruh"; 108 | console.error(err); 109 | }); 110 | })(); 111 | -------------------------------------------------------------------------------- /src/s.h: -------------------------------------------------------------------------------- 1 | #ifndef S_H_ 2 | #define S_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "memory.h" 9 | 10 | typedef struct { 11 | size_t len; 12 | const char *data; 13 | } String; 14 | 15 | static inline 16 | String string(size_t len, const char *data) 17 | { 18 | String result = { 19 | .len = len, 20 | .data = data 21 | }; 22 | 23 | return result; 24 | } 25 | 26 | static inline 27 | String cstr_as_string(const char *cstr) 28 | { 29 | return string(strlen(cstr), cstr); 30 | } 31 | 32 | static inline 33 | const char *string_as_cstr(Memory *memory, String s) 34 | { 35 | assert(memory); 36 | char *cstr = memory_alloc(memory, s.len + 1); 37 | memcpy(cstr, s.data, s.len); 38 | cstr[s.len] = '\0'; 39 | return cstr; 40 | } 41 | 42 | #define SLT(literal) string(sizeof(literal) - 1, literal) 43 | 44 | static inline 45 | String string_empty(void) 46 | { 47 | String result = { 48 | .len = 0, 49 | .data = NULL 50 | }; 51 | return result; 52 | } 53 | 54 | static inline 55 | String chop_until_char(String *input, char delim) 56 | { 57 | if (input->len == 0) { 58 | return string_empty(); 59 | } 60 | 61 | size_t i = 0; 62 | while (i < input->len && input->data[i] != delim) 63 | ++i; 64 | 65 | String line; 66 | line.data = input->data; 67 | line.len = i; 68 | 69 | if (i == input->len) { 70 | input->data += input->len; 71 | input->len = 0; 72 | } else { 73 | input->data += i + 1; 74 | input->len -= i + 1; 75 | } 76 | 77 | return line; 78 | } 79 | 80 | static inline 81 | String chop_line(String *input) { 82 | return chop_until_char(input, '\n'); 83 | } 84 | 85 | static inline 86 | String trim_begin(String s) 87 | { 88 | while (s.len && isspace(*s.data)) { 89 | s.data++; 90 | s.len--; 91 | } 92 | return s; 93 | } 94 | 95 | static inline 96 | String trim_end(String s) 97 | { 98 | while (s.len && isspace(s.data[s.len - 1])) { 99 | s.len--; 100 | } 101 | return s; 102 | } 103 | 104 | static inline 105 | String trim(String s) 106 | { 107 | return trim_begin(trim_end(s)); 108 | } 109 | 110 | static inline 111 | String chop_word(String *input) 112 | { 113 | if (input->len == 0) { 114 | return string_empty(); 115 | } 116 | 117 | *input = trim_begin(*input); 118 | 119 | size_t i = 0; 120 | while (i < input->len && !isspace(input->data[i])) { 121 | ++i; 122 | } 123 | 124 | String word; 125 | word.data = input->data; 126 | word.len = i; 127 | 128 | input->data += i; 129 | input->len -= i; 130 | 131 | return word; 132 | } 133 | 134 | static inline 135 | int string_equal(String a, String b) 136 | { 137 | if (a.len != b.len) return 0; 138 | return memcmp(a.data, b.data, a.len) == 0; 139 | } 140 | 141 | static inline 142 | String take(String s, size_t n) 143 | { 144 | if (s.len < n) return s; 145 | return (String) { 146 | .len = n, 147 | .data = s.data 148 | }; 149 | } 150 | 151 | static inline 152 | String drop(String s, size_t n) 153 | { 154 | if (s.len < n) return SLT(""); 155 | return (String) { 156 | .len = s.len - n, 157 | .data = s.data + n 158 | }; 159 | } 160 | 161 | static inline 162 | int prefix_of(String prefix, String s) 163 | { 164 | return string_equal(prefix, take(s, prefix.len)); 165 | } 166 | 167 | static inline 168 | void chop(String *s, size_t n) 169 | { 170 | *s = drop(*s, n); 171 | } 172 | 173 | static inline 174 | String concat3(Memory *memory, String a, String b, String c) 175 | { 176 | const size_t n = a.len + b.len + c.len; 177 | char *buffer = memory_alloc(memory, n); 178 | memcpy(buffer, a.data, a.len); 179 | memcpy(buffer + a.len, b.data, b.len); 180 | memcpy(buffer + a.len + b.len, c.data, c.len); 181 | return (String) { .len = n, .data = buffer }; 182 | } 183 | 184 | #endif // S_H_ 185 | -------------------------------------------------------------------------------- /src/json_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "json.h" 4 | 5 | #define MEMORY_CAPACITY (640 * 1000) 6 | 7 | int main(void) 8 | { 9 | Memory memory = { 10 | .capacity = MEMORY_CAPACITY, 11 | .buffer = malloc(MEMORY_CAPACITY) 12 | }; 13 | 14 | assert(memory.buffer); 15 | 16 | String tests[] = { 17 | SLT("null"), 18 | SLT("nullptr"), 19 | SLT("true"), 20 | SLT("false"), 21 | SLT("1"), 22 | SLT("2"), 23 | SLT(".10"), 24 | SLT("1e9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999"), 25 | SLT("-.10"), 26 | SLT("-10"), 27 | SLT("10.10"), 28 | SLT("-10.10e2"), 29 | SLT("-10.10e-2"), 30 | // TODO(#25): parse_json_number treats -10.-10e-2 as two separate numbers 31 | SLT("-10.-10e-2"), 32 | SLT("\"hello,\tworld\""), 33 | SLT("[]"), 34 | SLT("[1]"), 35 | SLT("[\"test\"]"), 36 | SLT("[1,2,3]"), 37 | SLT("[1,2,3 5]"), 38 | SLT("[\"hello,\tworld\", 123, \t \"abcd\", -10.10e-2, \"test\"]"), 39 | SLT("[[]]"), 40 | SLT("[123,[321,\"test\"],\"abcd\"]"), 41 | SLT("[\n" 42 | " true,\n" 43 | " false,\n" 44 | " null\n" 45 | "]"), 46 | SLT("[\n" 47 | " true,\n" 48 | " false #\n" 49 | " null\n" 50 | "]"), 51 | SLT("{\n" 52 | " \"null\": null,\n" 53 | " \"boolean\": true,\n" 54 | " \"boolean\": false,\n" 55 | " \"number\": 69420,\n" 56 | " \"string\": \"hello\",\n" 57 | " \"array\": [null, true, false, 69420, \"hello\"],\n" 58 | " \"object\": {}\n" 59 | "}"), 60 | SLT("{\n" 61 | " \"null\": null,\n" 62 | " \"boolean\": true\n" 63 | " \"boolean\": false,\n" 64 | " \"number\": 69420,\n" 65 | " \"string\": \"hello\",\n" 66 | " \"array\": [null, true, false, 69420, \"hello\"],\n" 67 | " \"object\": {}\n" 68 | "}"), 69 | SLT("[0e+1]"), 70 | SLT("[0e+-1]"), 71 | SLT("[0C]"), 72 | SLT("\"\\uD834\\uDD1E\\uD834\\uDD1E\\uD834\\uDD1E\\uD834\\uDD1E\""), 73 | SLT("\"\\uD834\\uDD1E\\uD834\\uDD1E\\uD834\\uDD1E\\uD834\""), 74 | SLT("[\"a\0a\"]"), 75 | SLT("[\"\\\\a\"]"), 76 | SLT("[\"\\\"\"]"), 77 | SLT("[\"new\aline\"]"), 78 | SLT("{\"test\": [0, true, 1], \"foo\": [{\"param\": \"data:text/html],https://1:a.it@www.it\\\\\"}, -889578990, false]}") 79 | }; 80 | size_t tests_count = sizeof(tests) / sizeof(tests[0]); 81 | 82 | for (size_t i = 0; i < tests_count; ++i) { 83 | fputs("PARSING: \n", stdout); 84 | fwrite(tests[i].data, 1, tests[i].len, stdout); 85 | fputc('\n', stdout); 86 | 87 | Json_Result result = parse_json_value(&memory, tests[i]); 88 | if (result.is_error) { 89 | fputs("FAILURE: \n", stdout); 90 | print_json_error(stdout, result, tests[i], ""); 91 | } else if (trim_begin(result.rest).len != 0) { 92 | fputs("FAILURE: \n", stdout); 93 | fputs("parsed ", stdout); 94 | print_json_value(stdout, result.value); 95 | fputc('\n', stdout); 96 | 97 | fputs("but left unparsed input: ", stdout); 98 | fwrite(result.rest.data, 1, result.rest.len, stdout); 99 | fputc('\n', stdout); 100 | } else { 101 | fputs("SUCCESS: \n", stdout); 102 | print_json_value(stdout, result.value); 103 | fputc('\n', stdout); 104 | } 105 | 106 | printf("MEMORY USAGE: %lu bytes\n", memory.size); 107 | fputs("------------------------------\n", stdout); 108 | memory_clean(&memory); 109 | } 110 | 111 | free(memory.buffer); 112 | 113 | return 0; 114 | } 115 | -------------------------------------------------------------------------------- /src/schedule.c: -------------------------------------------------------------------------------- 1 | #include 2 | #define __USE_XOPEN 3 | #include 4 | #include 5 | 6 | #include "schedule.h" 7 | 8 | static inline 9 | void expect_json_type(Json_Value value, Json_Type type) 10 | { 11 | if (value.type != type) { 12 | fprintf(stderr, 13 | "Expected %s, but got %s\n", 14 | json_type_as_cstr(type), 15 | json_type_as_cstr(value.type)); 16 | abort(); 17 | } 18 | } 19 | 20 | static inline 21 | String unwrap_json_string(Json_Value value) 22 | { 23 | expect_json_type(value, JSON_STRING); 24 | return value.string; 25 | } 26 | 27 | uint8_t json_as_days(Memory *memory, Json_Value input) 28 | { 29 | assert(memory); 30 | expect_json_type(input, JSON_ARRAY); 31 | 32 | uint8_t days = 0; 33 | for (Json_Array_Page *page = input.array.begin; 34 | page != NULL; 35 | page = page->next) 36 | { 37 | for (size_t page_index = 0; page_index < page->size; ++page_index) { 38 | expect_json_type(page->elements[page_index], JSON_NUMBER); 39 | int64_t x = json_number_to_integer(page->elements[page_index].number); 40 | // NOTE: 41 | // - schedule.json (1-7, Monday = 1) 42 | // - POSIX (0-6, Sunday = 0) 43 | // 44 | // the mask is expected to be POSIX compliant. 45 | // 46 | // JSON POSIX 47 | // Mon 1 -> 1 48 | // Tue 2 -> 2 49 | // Wed 3 -> 3 50 | // Thu 4 -> 4 51 | // Fri 5 -> 5 52 | // Sat 6 -> 6 53 | // Sun 7 -> 0 54 | days |= 1 << (x % 7); 55 | } 56 | } 57 | 58 | return days; 59 | } 60 | 61 | int json_as_time_min(Memory *memory, Json_Value input) 62 | { 63 | assert(memory); 64 | const char *input_cstr = string_as_cstr(memory, unwrap_json_string(input)); 65 | struct tm tm = {0}; 66 | strptime(input_cstr, "%H:%M", &tm); 67 | return tm.tm_hour * 60 + tm.tm_min; 68 | } 69 | 70 | struct tm json_as_date(Memory *memory, Json_Value input) 71 | { 72 | assert(memory); 73 | expect_json_type(input, JSON_STRING); 74 | const char *input_cstr = string_as_cstr(memory, unwrap_json_string(input)); 75 | struct tm tm = {0}; 76 | strptime(input_cstr, "%Y-%m-%d", &tm); 77 | return tm; 78 | } 79 | 80 | static 81 | struct Project json_as_project(Memory *memory, Json_Value input) 82 | { 83 | assert(memory); 84 | 85 | expect_json_type(input, JSON_OBJECT); 86 | 87 | struct Project project; 88 | memset(&project, 0, sizeof(project)); 89 | 90 | for (Json_Object_Page *page = input.object.begin; 91 | page != NULL; 92 | page = page->next) 93 | { 94 | for (size_t page_index = 0; page_index < page->size; ++page_index) { 95 | if (string_equal(page->elements[page_index].key, SLT("name"))) { 96 | project.name = unwrap_json_string(page->elements[page_index].value); 97 | } else if (string_equal(page->elements[page_index].key, SLT("description"))) { 98 | project.description = unwrap_json_string(page->elements[page_index].value); 99 | } else if (string_equal(page->elements[page_index].key, SLT("url"))) { 100 | project.url = unwrap_json_string(page->elements[page_index].value); 101 | } else if (string_equal(page->elements[page_index].key, SLT("days"))) { 102 | project.days = json_as_days(memory, page->elements[page_index].value); 103 | } else if (string_equal(page->elements[page_index].key, SLT("time"))) { 104 | project.time_min = json_as_time_min(memory, page->elements[page_index].value); 105 | } else if (string_equal(page->elements[page_index].key, SLT("channel"))) { 106 | project.channel = unwrap_json_string(page->elements[page_index].value); 107 | } else if (string_equal(page->elements[page_index].key, SLT("starts"))) { 108 | project.starts = memory_alloc_aligned(memory, sizeof(*project.starts), alignof(struct tm)); 109 | memset(project.starts, 0, sizeof(*project.starts)); 110 | *project.starts = json_as_date(memory, page->elements[page_index].value); 111 | } else if (string_equal(page->elements[page_index].key, SLT("ends"))) { 112 | project.ends = memory_alloc_aligned(memory, sizeof(*project.ends), alignof(struct tm)); 113 | memset(project.ends, 0, sizeof(*project.ends)); 114 | *project.ends = json_as_date(memory, page->elements[page_index].value); 115 | } 116 | } 117 | } 118 | 119 | return project; 120 | } 121 | 122 | static 123 | void parse_schedule_projects(Memory *memory, Json_Value input, struct Schedule *schedule) 124 | { 125 | assert(memory); 126 | assert(schedule); 127 | 128 | expect_json_type(input, JSON_ARRAY); 129 | 130 | const size_t array_size = json_array_size(input.array); 131 | const size_t memory_size = sizeof(schedule->projects[0]) * array_size; 132 | 133 | schedule->projects = memory_alloc_aligned(memory, memory_size, alignof(struct Project)); 134 | memset(schedule->projects, 0, memory_size); 135 | schedule->projects_size = 0; 136 | 137 | for (Json_Array_Page *page = input.array.begin; 138 | page != NULL; 139 | page = page->next) 140 | { 141 | for (size_t page_index = 0; page_index < page->size; ++page_index) { 142 | schedule->projects[schedule->projects_size++] = 143 | json_as_project(memory, page->elements[page_index]); 144 | } 145 | } 146 | } 147 | 148 | static 149 | void parse_schedule_cancelled_events(Memory *memory, Json_Value input, struct Schedule *schedule) 150 | { 151 | assert(memory); 152 | assert(schedule); 153 | expect_json_type(input, JSON_ARRAY); 154 | 155 | const size_t array_size = json_array_size(input.array); 156 | const size_t memory_size = sizeof(schedule->cancelled_events[0]) * array_size; 157 | 158 | schedule->cancelled_events = memory_alloc_aligned(memory, memory_size, alignof(time_t)); 159 | memset(schedule->cancelled_events, 0, memory_size); 160 | schedule->cancelled_events_count = 0; 161 | 162 | for (Json_Array_Page *page = input.array.begin; 163 | page != NULL; 164 | page = page->next) 165 | { 166 | for (size_t page_index = 0; page_index < page->size; ++page_index) { 167 | expect_json_type(page->elements[page_index], JSON_NUMBER); 168 | schedule->cancelled_events[schedule->cancelled_events_count++] = 169 | json_number_to_integer(page->elements[page_index].number); 170 | } 171 | } 172 | } 173 | 174 | static 175 | struct Event json_as_event(Memory *memory, Json_Value input) 176 | { 177 | assert(memory); 178 | expect_json_type(input, JSON_OBJECT); 179 | 180 | struct Event event = {0}; 181 | 182 | for (Json_Object_Page *page = input.object.begin; 183 | page != NULL; 184 | page = page->next) 185 | { 186 | for (size_t page_index = 0; page_index < page->size; ++page_index) { 187 | if (string_equal(page->elements[page_index].key, SLT("date"))) { 188 | event.date = json_as_date(memory, page->elements[page_index].value); 189 | } else if (string_equal(page->elements[page_index].key, SLT("time"))) { 190 | event.time_min = json_as_time_min(memory, page->elements[page_index].value); 191 | } else if (string_equal(page->elements[page_index].key, SLT("title"))) { 192 | event.title = unwrap_json_string(page->elements[page_index].value); 193 | } else if (string_equal(page->elements[page_index].key, SLT("description"))) { 194 | event.description = unwrap_json_string(page->elements[page_index].value); 195 | } else if (string_equal(page->elements[page_index].key, SLT("url"))) { 196 | event.url = unwrap_json_string(page->elements[page_index].value); 197 | } else if (string_equal(page->elements[page_index].key, SLT("channel"))) { 198 | event.channel = unwrap_json_string(page->elements[page_index].value); 199 | } 200 | } 201 | } 202 | 203 | return event; 204 | } 205 | 206 | static 207 | void parse_schedule_extra_events(Memory *memory, Json_Value input, struct Schedule *schedule) 208 | { 209 | assert(memory); 210 | assert(schedule); 211 | expect_json_type(input, JSON_ARRAY); 212 | 213 | const size_t array_size = json_array_size(input.array); 214 | const size_t memory_size = sizeof(schedule->extra_events[0]) * array_size; 215 | 216 | schedule->extra_events = memory_alloc_aligned(memory, memory_size, alignof(struct Event)); 217 | memset(schedule->extra_events, 0, memory_size); 218 | schedule->extra_events_size = 0; 219 | 220 | for (Json_Array_Page *page = input.array.begin; 221 | page != NULL; 222 | page = page->next) 223 | { 224 | for (size_t page_index = 0; page_index < page->size; ++page_index) { 225 | assert(schedule->extra_events_size < array_size); 226 | schedule->extra_events[schedule->extra_events_size++] = 227 | json_as_event(memory, page->elements[page_index]); 228 | } 229 | } 230 | } 231 | 232 | struct Schedule json_as_schedule(Memory *memory, Json_Value input) 233 | { 234 | assert(memory); 235 | expect_json_type(input, JSON_OBJECT); 236 | 237 | struct Schedule schedule = {0}; 238 | 239 | for (Json_Object_Page *page = input.object.begin; 240 | page != NULL; 241 | page = page->next) 242 | { 243 | for (size_t page_index = 0; page_index < page->size; ++page_index) 244 | { 245 | if (string_equal(page->elements[page_index].key, SLT("projects"))) { 246 | parse_schedule_projects(memory, page->elements[page_index].value, &schedule); 247 | } else if (string_equal(page->elements[page_index].key, SLT("cancelledEvents"))) { 248 | parse_schedule_cancelled_events(memory, page->elements[page_index].value, &schedule); 249 | } else if (string_equal(page->elements[page_index].key, SLT("extraEvents"))) { 250 | parse_schedule_extra_events(memory, page->elements[page_index].value, &schedule); 251 | } else if (string_equal(page->elements[page_index].key, SLT("timezone"))) { 252 | schedule.timezone = unwrap_json_string(page->elements[page_index].value); 253 | } 254 | } 255 | } 256 | 257 | return schedule; 258 | } 259 | -------------------------------------------------------------------------------- /schedule.json: -------------------------------------------------------------------------------- 1 | {"projects":[{"name":"Nothing (Game in Pure C)","description":"A simple platformer about nothing. No Engines, no OpenGL, no Box2D. Only C and SDL2","url":"https://github.com/tsoding/nothing","days":[6,7],"time":"23:00","channel":"https://twitch.tv/tsoding","ends":"2020-02-08"},{"name":"HyperNerd (Bot in Haskell)","description":"Enhancing my Total Surveillance Automatic Ban Machine. Join to help to test it!","url":"https://github.com/tsoding/HyperNerd","days":[4],"time":"23:00","channel":"https://twitch.tv/tsoding"},{"name":"YouTube content or whatever","description":"This day is reserved for recording YouTube videos if I have anything to record at the moment. Otherwise doing whatever. Most probably trying new languages, technologies, project ideas.","url":"https://youtube.com/tsoding","days":[2],"time":"23:00","ends":"2018-07-23","comment":"Moved to Wednesday in favor of Ray Tracer in C++","channel":"https://twitch.tv/tsoding"},{"name":"YouTube content or whatever","description":"This day is reserved for recording YouTube videos if I have anything to record at the moment. Otherwise doing whatever. Most probably trying new languages, technologies, project ideas.","url":"https://youtube.com/tsoding","days":[3],"time":"23:00","starts":"2018-07-23","ends":"2019-01-12","channel":"https://twitch.tv/tsoding"},{"name":"Multik (OCaml, C)","description":"Animation Framework for making animation for Tsoding YouTube channel","url":"https://github.com/tsoding/multik","days":[3],"time":"23:00","starts":"2019-01-12","ends":"2019-06-13","channel":"https://twitch.tv/tsoding"},{"name":"Ray Tracer in C++","description":"My ongoing effort to learn how to make Ray Tracers.","url":"https://github.com/tsoding/ray-tracer","days":[2],"time":"23:00","ends":"2018-11-10","channel":"https://twitch.tv/tsoding"},{"name":"Snitch (GoLang)","url":"https://github.com/tsoding/snitch","description":"A simple tool that collects TODOs in the source code and reports them as GitHub issues","days":[2],"time":"22:00","starts":"2018-11-11","ends":"2019-01-28","channel":"https://twitch.tv/tsoding"},{"name":"Teeworlds Gamepad Support (Real Open Source Contribution)","url":"https://github.com/teeworlds/teeworlds","description":"On the Real Open Source Contribution series we are trying to take an Open Source project that I personally use on a daily basis and improve it. The goal is to implement a feature and actually make it into the upstream source code of the project. The current project is Teeworlds --- a retro multiplayer game. The feature we are trying to ship is Gamepad support.","days":[2],"time":"23:00","starts":"2019-01-29","ends":"2019-03-11","channel":"https://twitch.tv/tsoding"},{"name":"MyPaint Selection Tool (Real Open Source Contribution)","url":"http://mypaint.org/","description":"

On the Real Open Source Contribution series we are trying to take an Open Source project that I personally use on a daily basis and improve it. The goal is to implement a feature and actually make it into the upstream source code of the project.

Current Project: MyPaint

Feature: Selection Tool.

","days":[2],"time":"23:00","starts":"2019-01-29","ends":"2019-05-15","channel":"https://twitch.tv/tsoding"},{"name":"Art Stream","url":"https://twitch.tv/r3x1m","description":"Tsoding-related Art Streams on my personal channel. Most likely drawing brand assets.","days":[5],"time":"23:00","starts":"2019-02-17","ends":"2019-03-27","channel":"https://twitch.tv/r3x1m"},{"name":"Smart Stream","url":"https://www.twitch.tv/tsoding","description":"

On Smart Stream we watch educational videos trying to get smart.

Use command !friday to suggest a video in Twitch or Discord chats.

","days":[5],"time":"23:00","starts":"2019-03-28","channel":"https://twitch.tv/tsoding"},{"name":"Chatterino 2 (Real Open Source Contribution)","url":"https://github.com/fourtf/chatterino2","description":"On the Real Open Source Contribution series we are trying to take an Open Source project that I personally use on a daily basis and improve it. The goal is to implement a feature and actually make it into the upstream source code of the project.\n\n**Current Project:** [Chatterino 2](https://github.com/fourtf/chatterino2)\n\n**Feature:** [#976](https://github.com/fourtf/chatterino2/issues/976).","days":[2],"time":"23:00","starts":"2019-05-15","ends":"2019-06-17","channel":"https://twitch.tv/tsoding"},{"name":"GRUB 2 (Real Open Source Contribution)","url":"https://www.gnu.org/software/grub/grub-download.html","description":"On the Real Open Source Contribution series we are trying to take an Open Source project that I personally use on a daily basis and improve it. The goal is to implement a feature and actually make it into the upstream source code of the project.\n\n**Current Project:** [GRUB 2](https://www.gnu.org/software/grub/grub-download.html)\n\n**Feature:** [Gamepad Support](https://steamcommunity.com/groups/steamuniverse/discussions/0/558751660797029626/)","days":[2],"time":"23:00","starts":"2019-06-18","ends":"2019-10-07","channel":"https://twitch.tv/tsoding"},{"name":"PinPog (Game in Assembly)","url":"https://github.com/tsoding/pinpog","description":"Ping Pong in Assembly that works without OS. Our goal is to write a game that fits into 512 bytes bootloader and works in 16 bit real mode on any IBM PC compatible machine without any Operating System.","days":[3],"time":"23:00","starts":"2019-06-13","ends":"2019-08-01","channel":"https://twitch.tv/tsoding"},{"name":"Linux Magnifier App in Nim","url":"https://github.com/tsoding/boomer","description":"Magnifier (Zoomer) Application for Boomers. Works similarly to the builtin zoomer in XFCE Window Manager","days":[3],"time":"23:00","starts":"2019-08-07","ends":"2020-01-30","channel":"https://twitch.tv/tsoding"},{"name":"Vodus (VOD chat renderer in C++)","url":"https://github.com/tsoding/vodus","description":"VOD chat renderer","days":[3],"time":"23:00","starts":"2020-02-05","channel":"https://twitch.tv/tsoding"},{"name":"Minetest (Real Open Source Contribution)","url":"https://github.com/minetest/minetest","description":"Minetest, an open source infinite-world block sandbox game engine with support for survival and crafting. We are trying to fix bug [#3075](https://github.com/minetest/minetest/issues/3075).","days":[2],"time":"23:00","starts":"2019-10-07","ends":"2019-11-13","channel":"https://twitch.tv/tsoding"},{"name":"Syncthing (Real Open Source Contribution)","url":"https://syncthing.net/","description":"Syncthing is a continuous file synchronization program. It synchronizes files between two or more computers and replaces proprietary sync and cloud services with something open, trustworthy and decentralized.\n\nSee https://github.com/tsoding/trophy#syncthing for more information.","days":[2],"time":"23:00","starts":"2019-11-19","ends":"2019-12-03","channel":"https://twitch.tv/tsoding"},{"name":"Web Application in C","url":"https://github.com/tsoding/skedudle","description":"Simple Event Schedule Web Application in C. This project has two goals:\n1. Implement backgend for https://github.com/tsoding/schedule\n2. Implement enough Web related code to later extract it as an epic Web Framework in C","days":[2],"time":"23:00","starts":"2019-12-09","channel":"https://twitch.tv/tsoding"},{"name":"Next Gamedev Project","description":"[Nothing is put on hiatus for now](https://gist.github.com/rexim/9a66d86860681b1b34f414572fddf4ff)\n\nThis is the next gamedev project with the goal to actually ship something. It's gonna be a platformer as well. Details will be revealed later.","url":"https://github.com/tsoding/something","days":[6,7],"time":"23:00","channel":"https://twitch.tv/tsoding","starts":"2020-02-09"}],"extraEvents":[{"date":"2018-07-11","time":"23:00","title":"Schedule Web App in TypeScript","description":"

Schedule for Tsoding Streams. Front-End only Single Page Application without any Back-End. Makes all of the schedule building work yours computer problem. Saves me money on hosting.

This is an extra stream to get some work done for the upcoming release.

","url":"https://github.com/tsoding/schedule-beta","channel":"https://twitch.tv/tsoding"},{"date":"2018-08-03","time":"23:00","title":"HyperNerd (Bot in Haskell)","description":"Enhancing my Total Surveillance Automatic Ban Machine. Join to help to test it! Moved from Thu, Aug 2, 2018.","url":"https://github.com/tsoding/HyperNerd","channel":"https://twitch.tv/tsoding"},{"date":"2018-12-21","time":"23:00","title":"Hacking Teeworlds (Game in C++)","url":"https://github.com/teeworlds/teeworlds","description":"I got a pretty interesting idea for a gamepad support for this game. Wanna try to implement it on the stream.","channel":"https://twitch.tv/tsoding"},{"date":"2019-01-11","time":"23:00","title":"Multik (OCaml, C)","url":"https://github.com/tsoding/multik","description":"Animation Framework for making animation for Tsoding YouTube channel","channel":"https://twitch.tv/tsoding"},{"date":"2019-05-06","time":"23:00","title":"Extra Haskell Stream","url":"https://www.youtube.com/tsoding","description":"First half — recording a video for the YouTube Channel. Second half — HyperNerd development.","channel":"https://twitch.tv/tsoding"},{"date":"2019-06-03","time":"23:00","title":"Extra Haskell Stream (HyperNerd)","url":"https://github.com/tsoding/HyperNerd","description":"Since the last Haskell stream was cancelled due to my Internet being down, let's make another one instead of day off.","channel":"https://twitch.tv/tsoding"},{"date":"2019-06-24","time":"23:00","title":"Recording YouTube video (Haskell)","url":"https://youtub.com/tsoding","description":"Recording next YouTube video.","channel":"https://twitch.tv/tsoding"},{"date":"2019-09-23","time":"23:00","title":"Nothing (Game in Pure C)","url":"https://github.com/tsoding/nothing","description":"A simple platformer about nothing. No Engines, no OpenGL, no Box2D. Only C and SDL2","channel":"https://twitch.tv/tsoding"},{"date":"2019-10-07","time":"23:00","title":"Last GRUB 2 stream","url":"https://www.gnu.org/software/grub/grub-download.html","description":"It was a fun project, but it's time to wrap it up...","channel":"https://twitch.tv/tsoding"},{"date":"2019-11-04","time":"23:00","title":"JSON Parser in 100 Lines from scratch in Haskell","url":"https://tsoding.org/schedule","description":"","channel":"https://twitch.tv/tsoding"},{"date":"2019-12-23","time":"23:00","title":"Stream from Debianus","url":"https://twitch.tv/tsoding","description":"I finally set up Debian on my second laptop to the point where I think it might be streamable. Testing it out. Gonna do some random Farting Around™.","channel":"https://twitch.tv/tsoding"},{"title":"Web Application in C","url":"https://github.com/tsoding/skedudle","description":"Simple Event Schedule Web Application in C. This project has two goals:\n1. Implement backgend for https://github.com/tsoding/schedule\n2. Implement enough Web related code to later extract it as an epic Web Framework in C","time":"23:00","date":"2019-12-30","channel":"https://twitch.tv/tsoding"},{"title":"Some Quick YouTube stuff recording","url":"https://twitch.tv/tsoding","description":"Nothing much to say. We gonna record some stuff for YouTube.","time":"23:00","date":"2020-01-20","channel":"https://twitch.tv/tsoding"},{"title":"Vodus — Twitch Chat Renderer","url":"https://github.com/tsoding/vodus","description":"I got bored and decided to stream whatever.","time":"23:00","date":"2020-01-27","channel":"https://twitch.tv/tsoding"},{"title":"Lazy Evalution using TypeScript","url":"https://twitch.tv/tsoding","description":"In today's lecture we gonna take a look into [Lazy Evalution](https://en.wikipedia.org/wiki/Lazy_evaluation) using [TypeScript](https://www.typescriptlang.org/) for all of the examples.","time":"23:00","date":"2020-02-03","channel":"https://twitch.tv/tsoding"},{"title":"Extra Something Stream","url":"https://github.com/tsoding/something","description":"I kinda wanna keep developing that [thing](https://github.com/tsoding/something) while I have motivation to do so. Let's do another GameDev stream today, okay?\n\nP.S. I'm super duper sorry if I ruined your today's plans with this stream!","time":"23:00","date":"2020-02-24","channel":"https://twitch.tv/tsoding"}],"cancelledEvents":[1532534400,1533225600,1536163200,1540396800,1541260800,1544025600,1545235200,1547568000,1548774000,1559232000,1569081600,1574784000,1577808000,1579017600,1579104000,1579190400,1579276800],"timezone":"Asia/Novosibirsk","eventPatches":{"1536163200":{"title":"Probabilistic Functional Programming","url":"https://wiki.haskell.org/Probabilistic_Functional_Programming","description":"Probabilistic functional programming is a library for discrete random variables in terms of their distribution. A distribution represent the outcome of a probabilistic event as a collection of all possible values, tagged with their likelihood. It turns out that random variables establish a monad, namely a list monad where all items are equipped with a probability. A nice aspect of this system is that simulations can be specified independently from their method of execution. That is, we can either fully simulate or randomize any simulation without altering the code which defines it. Examples include dice games, the Monty Hall paradoxon and others."},"1536768000":{"title":"Lisp in Haskell","url":"https://tsoding.github.io/schedule/","description":"Some time ago I started to develop my own Lisp language for scripting Nothing levels. The language is implemented in C, but I wanna try to implement it in Haskell just to see how easier/difficult it would be. I also wanna see if this makes a good YouTube video."},"1537286400":{"title":"Snitch (GoLang)","url":"https://github.com/tsoding/snitch","description":"A simple tool that collects TODOs in the source code and reports them as GitHub issues"},"1539100800":{"title":"Contribution Tracker (Haskell, Servant)","url":"https://github.com/tsoding/tsugar","description":"Simple service that monitors the activity on GitHub and give points for contributions"},"1540915200":{"title":"Snitch (GoLang)","url":"https://github.com/tsoding/snitch","description":"A simple tool that collects TODOs in the source code and reports them as GitHub issues"},"1541520000":{"title":"Snitch (GoLang)","url":"https://github.com/tsoding/snitch","description":"A simple tool that collects TODOs in the source code and reports them as GitHub issues"},"1542211200":{"title":"Contribution Tracker (Haskell, Servant)","url":"https://github.com/tsoding/tsugar","description":"Simple service that monitors the activity on GitHub and give points for contributions"},"1543420800":{"title":"Auto Testing for HyperNerd (ChatBot in Haskell)","url":"https://github.com/tsoding/HyperNerd","description":"Writing tests for our ChatBot is kinda difficult at the moment and because of that I've been avoiding implementing tests for quite awhile already. Which is not good. Today I wanna try to implement some abstraction layer to make testing of the bot easier. We also probably need to setup some code coverage threshold to motivate all of the contributors to write tests."},"1545235200":{"title":"Hacking Teeworlds (Game in C++)","url":"https://github.com/teeworlds/teeworlds","description":"Yesterday I got a pretty interesting idea for a gamepad support for this game. Wanna try to implement it on the stream."},"1546358400":{"title":"Learning Programming with Scratch","url":"https://beta.scratch.mit.edu/","description":"Let's start the year with Scratch programming! Pog"},"1546444800":{"title":"Contribution Tracker (Haskell, Servant)","url":"https://github.com/tsoding/tsugar","description":"Simple service that monitors the activity on GitHub and give points for contributions"},"1551024000":{"description":"

UPD. First ~1 hour of the stream is going to be recording the next episode of HaskellRank

A simple platformer about nothing. No Engines, no OpenGL, no Box2D. Only C and SDL2

"},"1551283200":{"description":"

UPD. First ~1 hour of the stream is going to be recording the next episode of HaskellRank

Animation Framework for making animation for Tsoding YouTube channel.

"},"1552060800":{"title":"YouTube Content","url":"https://www.youtube.com/tsoding","description":"Recording some videos for my YouTube Channel:
  • April Fools Video
  • Next HaskellRank episode with CodeWars
","channel":"https://twitch.tv/tsoding"},"1553270400":{"title":"Smart Stream","url":"https://www.twitch.tv/tsoding","description":"

On Smart Stream we watch educational videos trying to get smart.

","channel":"https://twitch.tv/tsoding"},"1555430400":{"title":"Teeworlds Gamepad + MyPaint Selection Tool (Real Open Source Contribution)","description":"

UPD. First half of the stream will be fixing the gamepad support that we implemented for Teeworlds. Issue #2085

On the Real Open Source Contribution series we are trying to take an Open Source project that I personally use on a daily basis and improve it. The goal is to implement a feature and actually make it into the upstream source code of the project.

Current Project: MyPaint

Feature: Selection Tool.

"},"1555516800":{"title":"Trying out Nim","description":"On this stream we are just trying out Nim Programming Language. Nothing special.","url":"https://nim-lang.org/"},"1556640000":{"title":"Nim II","description":"On the previous Nim stream the compilation killed my Laptop and we didn't have an opportunity to properly check out the language. Let's try to do it again!","url":"https://nim-lang.org/"},"1557244800":{"title":"Nim III","description":"HDD on my main streaming machine died. On my backup laptop I have only Nim. Let's do another Nim stream lol.","url":"https://github.com/tsoding/vitanim"},"1557849600":{"title":"Chatterino 2 (Real Open Source Contribution)","description":"

On the Real Open Source Contribution series we are trying to take an Open Source project that I personally use on a daily basis and improve it. The goal is to implement a feature and actually make it into the upstream source code of the project.

Current Project: Chatterino 2

Feature: Probably #976

","url":"https://github.com/fourtf/chatterino2"},"1560355200":{"title":"New Project (Pilot Stream)","description":"[REDACTED]","url":"https://tsoding.github.io/schedule/#_1560355200"},"1568736000":{"title":"TempleOS","description":"The GRUB gamepad patch was submitted to grub-devel mailing list, but nobody responded yet. So in the meantime while we are waiting for the feedback let's do a filler stream and play with Operating System created by The Smartest Programmer That Ever Lived","url":"https://templeos.org/"},"1569340800":{"title":"HTTP Server in C","description":"Still no response from GRUB devs. Let's do something random.","url":"https://twitch.tv/tsoding"},"1569945600":{"title":"HTTP Server in C again","description":"The GRUB devs acknowledged the existence of the patch, but we still have not recieve any actual feedback that we can work on. Let's continue playing with WebDev in Pure C","url":"https://github.com/tsoding/node.c"},"1575388800":{"title":"Web Application in C","description":"I looked into Syncthing project for the past two weeks and just could not find anything interesting to work on. So we probably gonna do a different project next week, but today let's do Web Dev in C again lol","url":"https://github.com/tsoding/node.c"},"1579363200":{"title":"Tzozin Comeback","description":"![](https://cdn.frankerfacez.com/935f9c418438c578b52659961ccc233a.png)","url":"https://twitch.tv/tsoding"}}} 2 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include "s.h" 24 | #include "response.h" 25 | #include "request.h" 26 | #include "memory.h" 27 | #include "schedule.h" 28 | #include "json.h" 29 | #include "platform_specific.h" 30 | 31 | #define REQUEST_BUFFER_CAPACITY (640 * KILO) 32 | char request_buffer[REQUEST_BUFFER_CAPACITY]; 33 | 34 | void http_error_page_template(int OUT, int code) 35 | { 36 | #define INT(x) dprintf(OUT, "%d", x); 37 | #include "error_page_template.h" 38 | #undef INT 39 | } 40 | 41 | int http_error(int fd, int code, const char *format, ...) 42 | { 43 | va_list args; 44 | va_start(args, format); 45 | vfprintf(stderr, format, args); 46 | va_end(args); 47 | 48 | response_status_line(fd, code); 49 | response_header(fd, "Content-Type", "text/html"); 50 | response_body_start(fd); 51 | http_error_page_template(fd, code); 52 | 53 | return 1; 54 | } 55 | 56 | int serve_file(int dest_fd, 57 | const char *filepath, 58 | const char *content_type) 59 | { 60 | printf("[INFO] Serving file: %s\n", filepath); 61 | 62 | int src_fd = -1; 63 | 64 | struct stat file_stat; 65 | int err = stat(filepath, &file_stat); 66 | if (err < 0) { 67 | return http_error(dest_fd, 404, strerror(errno)); 68 | } 69 | 70 | src_fd = open(filepath, O_RDONLY); 71 | if (src_fd < 0) { 72 | return http_error(dest_fd, 404, strerror(errno)); 73 | } 74 | 75 | response_status_line(dest_fd, 200); 76 | response_header(dest_fd, "Content-Type", content_type); 77 | response_header(dest_fd, "Content-Length", "%d", file_stat.st_size); 78 | response_body_start(dest_fd); 79 | 80 | off_t offset = 0; 81 | while (offset < file_stat.st_size) { 82 | // TODO(#3): Try to align sendfile chunks according to tcp mem buffer 83 | // Will that even improve the performance? 84 | // References: 85 | // - http://man7.org/linux/man-pages/man2/sysctl.2.html 86 | // - `sysctl -w net.ipv4.tcp_mem='8388608 8388608 8388608'` 87 | ssize_t n = sendfile_wrapper(dest_fd, src_fd, &offset, 1024); 88 | if (n < 0) { 89 | fprintf(stderr, "[ERROR] Could not finish serving the file: %s\n", 90 | strerror(errno)); 91 | break; 92 | } 93 | } 94 | 95 | if (src_fd >= 0) { 96 | close(src_fd); 97 | } 98 | 99 | return 0; 100 | } 101 | 102 | int is_cancelled(struct Schedule *schedule, time_t id) 103 | { 104 | for (size_t i = 0; i < schedule->cancelled_events_count; ++i) { 105 | if (schedule->cancelled_events[i] == id) { 106 | return 1; 107 | } 108 | } 109 | return 0; 110 | } 111 | 112 | // TODO(#13): schedule does not support patches 113 | // TODO(#10): there is no endpoint to get a schedule for a period 114 | // TODO(#14): / should probably return the page of https://tsoding.org/schedule 115 | // Which will require to move rest map to somewhere 116 | 117 | time_t id_of_event(struct Event event) 118 | { 119 | return timegm(&event.date) + timezone + event.time_min * 60; 120 | } 121 | 122 | Json_Value event_as_json(Memory *memory, struct Event event) 123 | { 124 | assert(memory); 125 | 126 | const time_t id = id_of_event(event); 127 | const size_t id_cstr_size = 256; 128 | char *id_cstr = memory_alloc(memory, id_cstr_size); 129 | snprintf(id_cstr, id_cstr_size, "%ld", id); 130 | 131 | Json_Object object = {0}; 132 | 133 | json_object_push(memory, &object, SLT("id"), json_string(cstr_as_string(id_cstr))); 134 | json_object_push(memory, &object, SLT("title"), json_string(event.title)); 135 | json_object_push(memory, &object, SLT("description"), json_string(event.description)); 136 | json_object_push(memory, &object, SLT("url"), json_string(event.url)); 137 | json_object_push(memory, &object, SLT("channel"), json_string(event.channel)); 138 | 139 | return (Json_Value) { 140 | .type = JSON_OBJECT, 141 | .object = object 142 | }; 143 | } 144 | 145 | void print_event(int dest_fd, Memory *memory, struct Event event) 146 | { 147 | Json_Value event_json = event_as_json(memory, event); 148 | print_json_value_fd(dest_fd, event_json); 149 | } 150 | 151 | int next_event(time_t current_time, 152 | struct Schedule *schedule, 153 | struct Event *output) 154 | { 155 | struct Event result = {0}; 156 | time_t result_id = -1; 157 | 158 | for (size_t i = 0; i < schedule->extra_events_size; ++i) { 159 | struct Event event = schedule->extra_events[i]; 160 | time_t event_id = id_of_event(event); 161 | if (current_time < event_id && !is_cancelled(schedule, event_id)) { 162 | if (result_id < 0 || event_id < result_id) { 163 | result = event; 164 | result_id = event_id; 165 | } 166 | } 167 | } 168 | 169 | for (int j = 0; j < 7; ++j) { 170 | time_t week_time = current_time + 24 * 60 * 60 * j; 171 | struct tm *week_tm = gmtime(&week_time); 172 | 173 | for (size_t i = 0; i < schedule->projects_size; ++i) { 174 | if (!(schedule->projects[i].days & (1 << week_tm->tm_wday))) { 175 | continue; 176 | } 177 | 178 | if (schedule->projects[i].starts) { 179 | time_t starts_time = timegm(schedule->projects[i].starts) - timezone; 180 | if (week_time < starts_time) continue; 181 | } 182 | 183 | if (schedule->projects[i].ends) { 184 | time_t ends_time = timegm(schedule->projects[i].ends) - timezone; 185 | if (ends_time < week_time) continue; 186 | } 187 | 188 | struct Event event = { 189 | .time_min = schedule->projects[i].time_min, 190 | .title = schedule->projects[i].name, 191 | .description = schedule->projects[i].description, 192 | .url = schedule->projects[i].url, 193 | .channel = schedule->projects[i].channel 194 | }; 195 | 196 | event.date = *week_tm; 197 | event.date.tm_sec = 0; 198 | event.date.tm_min = 0; 199 | event.date.tm_hour = 0; 200 | 201 | time_t event_id = id_of_event(event); 202 | 203 | if (is_cancelled(schedule, event_id)) { 204 | continue; 205 | } 206 | 207 | if (current_time >= event_id) { 208 | continue; 209 | } 210 | 211 | if (result_id < 0 || event_id < result_id) { 212 | result = event; 213 | result_id = event_id; 214 | } 215 | } 216 | } 217 | 218 | if (output) { 219 | *output = result; 220 | } 221 | 222 | return result_id >= 0; 223 | } 224 | 225 | int serve_next_stream(int dest_fd, Memory *memory, struct Schedule *schedule) 226 | { 227 | response_status_line(dest_fd, 200); 228 | response_header(dest_fd, "Content-Type", "application/json"); 229 | response_body_start(dest_fd); 230 | 231 | time_t current_time = time(NULL) - timezone; 232 | struct Event event; 233 | if (next_event(current_time, schedule, &event)) { 234 | print_event(dest_fd, memory, event); 235 | } 236 | 237 | return 0; 238 | } 239 | 240 | int serve_rest_map(Memory *memory, int dest_fd, String host) 241 | { 242 | assert(memory); 243 | 244 | response_status_line(dest_fd, 200); 245 | response_header(dest_fd, "Content-Type", "application/json"); 246 | response_body_start(dest_fd); 247 | 248 | Json_Object rest_map = {0}; 249 | json_object_push( 250 | memory, &rest_map, 251 | SLT("next_stream"), 252 | json_string(concat3(memory, SLT("http://"), host, SLT("/api/next_stream")))); 253 | json_object_push( 254 | memory, &rest_map, 255 | SLT("period_streams"), 256 | json_string(concat3(memory, SLT("http://"), host, SLT("/api/period_streams")))); 257 | 258 | print_json_value_fd(dest_fd, (Json_Value) { .type = JSON_OBJECT, .object = rest_map }); 259 | 260 | return 0; 261 | } 262 | 263 | int is_same_day(struct tm a, struct tm b) 264 | { 265 | return a.tm_mday == b.tm_mday 266 | && a.tm_mon == b.tm_mon 267 | && a.tm_year == b.tm_year; 268 | } 269 | 270 | typedef void (*EventCallback)(void *context, struct Event* event); 271 | 272 | static 273 | size_t events_at_day(struct tm date, 274 | struct Schedule *schedule, 275 | EventCallback event_callback, 276 | void *event_context) 277 | { 278 | size_t result = 0; 279 | 280 | date.tm_sec = 0; 281 | date.tm_min = 0; 282 | date.tm_hour = 0; 283 | 284 | for (size_t i = 0; i < schedule->extra_events_size; ++i) { 285 | if (is_same_day(date, schedule->extra_events[i].date)) { 286 | result += 1; 287 | event_callback(event_context, &schedule->extra_events[i]); 288 | } 289 | } 290 | 291 | time_t date_time = timegm(&date) - timezone; 292 | 293 | for (size_t i = 0; i < schedule->projects_size; ++i) { 294 | if (!(schedule->projects[i].days & (1 << date.tm_wday))) { 295 | continue; 296 | } 297 | 298 | if (schedule->projects[i].starts) { 299 | time_t starts_time = timegm(schedule->projects[i].starts) - timezone; 300 | if (date_time < starts_time) continue; 301 | } 302 | 303 | if (schedule->projects[i].ends) { 304 | time_t ends_time = timegm(schedule->projects[i].ends) - timezone; 305 | if (ends_time < date_time) continue; 306 | } 307 | 308 | struct Event event = { 309 | .time_min = schedule->projects[i].time_min, 310 | .title = schedule->projects[i].name, 311 | .description = schedule->projects[i].description, 312 | .url = schedule->projects[i].url, 313 | .channel = schedule->projects[i].channel 314 | }; 315 | 316 | event.date = date; 317 | time_t event_id = id_of_event(event); 318 | 319 | if (is_cancelled(schedule, event_id)) { 320 | continue; 321 | } 322 | 323 | result += 1; 324 | event_callback(event_context, &event); 325 | } 326 | 327 | return result; 328 | } 329 | 330 | struct Context 331 | { 332 | Json_Array array; 333 | Memory *memory; 334 | }; 335 | 336 | void append_event_to_context(struct Context *context, struct Event *event) 337 | { 338 | Json_Value value = event_as_json(context->memory, *event); 339 | json_array_push(context->memory, &context->array, value); 340 | } 341 | 342 | static 343 | int serve_period_streams(int fd, Memory *memory, struct Schedule *schedule) 344 | { 345 | assert(memory); 346 | assert(schedule); 347 | 348 | struct Context context = { 349 | .array = {0}, 350 | .memory = memory 351 | }; 352 | 353 | const time_t SECONDS_IN_DAYS = 24 * 60 * 60; 354 | const size_t DAYS_IN_PAST = 4; 355 | time_t current_time = time(NULL) - timezone - SECONDS_IN_DAYS * DAYS_IN_PAST; 356 | for (size_t i = 0; i < 14 + DAYS_IN_PAST; ++i) { 357 | struct tm *current_date = gmtime(¤t_time); 358 | 359 | size_t count = events_at_day(*current_date, 360 | schedule, 361 | (EventCallback)append_event_to_context, 362 | &context); 363 | 364 | if (count == 0) { 365 | // TODO(#72): Day off cell does not have a date attached to it 366 | json_array_push(context.memory, &context.array, json_null); 367 | } 368 | 369 | current_time += SECONDS_IN_DAYS; 370 | } 371 | 372 | response_status_line(fd, 200); 373 | response_header(fd, "Content-Type", "application/json"); 374 | response_body_start(fd); 375 | print_json_value_fd(fd, (Json_Value) { .type = JSON_ARRAY, .array = context.array }); 376 | 377 | return 0; 378 | } 379 | 380 | const char *mime_of_file_path(const char *file_path) 381 | { 382 | if (fnmatch("*.css", file_path, 0) == 0) { 383 | return "text/css"; 384 | } else if (fnmatch("*.js", file_path, 0) == 0) { 385 | return "application/javascript"; 386 | } else if (fnmatch("*.html", file_path, 0) == 0) { 387 | return "text/html"; 388 | } 389 | 390 | return "text/plain"; 391 | } 392 | 393 | int handle_request(int fd, struct sockaddr_in *addr, Memory *memory, struct Schedule *schedule) 394 | { 395 | assert(addr); 396 | 397 | ssize_t request_buffer_size = read(fd, request_buffer, REQUEST_BUFFER_CAPACITY); 398 | 399 | if (request_buffer_size == 0) return http_error(fd, 400, "EOF"); 400 | if (request_buffer_size < 0) return http_error(fd, 500, strerror(errno)); 401 | 402 | String buffer = { 403 | .len = (uint64_t)request_buffer_size, 404 | .data = request_buffer 405 | }; 406 | 407 | Status_Line status_line = chop_status_line(&buffer); 408 | 409 | if (!string_equal(status_line.method, SLT("GET"))) { 410 | return http_error(fd, 405, "Unknown method\n"); 411 | } 412 | printf("[%.*s] %.*s\n", 413 | (int) status_line.method.len, status_line.method.data, 414 | (int) status_line.path.len, status_line.path.data); 415 | 416 | String host = {0}; 417 | String header_line = trim(chop_line(&buffer)); 418 | Header header = {{0}, {0}}; 419 | while (header_line.len > 0) { 420 | header = parse_header(header_line); 421 | if (string_equal(header.name, SLT("Host"))) { 422 | host = header.value; 423 | } 424 | 425 | header_line = trim(chop_line(&buffer)); 426 | } 427 | 428 | // TODO(#56): serve static files from a specific folder instead of hardcoding routes 429 | 430 | String router = chop_until_char(&status_line.path, '/'); 431 | if (router.len != 0) { 432 | return http_error(fd, 400, "Broken status line\n"); 433 | } 434 | 435 | router = chop_until_char(&status_line.path, '/'); 436 | 437 | #define STATIC_FOLDER "./public" 438 | 439 | if (router.len == 0) { 440 | return serve_file(fd, STATIC_FOLDER"/index.html", "text/html"); 441 | } else if (string_equal(router, SLT("api"))) { 442 | router = chop_until_char(&status_line.path, '/'); 443 | 444 | if (string_equal(router, SLT(""))) { 445 | return serve_rest_map(memory, fd, host); 446 | } 447 | 448 | if (string_equal(router, SLT("next_stream"))) { 449 | return serve_next_stream(fd, memory, schedule); 450 | } 451 | 452 | if (string_equal(router, SLT("period_streams"))) { 453 | return serve_period_streams(fd, memory, schedule); 454 | } 455 | } else if (string_equal(router, SLT("static"))) { 456 | #define STATIC_FILE_ROUTE(filename, mime) \ 457 | if (string_equal(status_line.path, SLT(filename))) { \ 458 | return serve_file(fd, STATIC_FOLDER "/" filename, mime); \ 459 | } 460 | 461 | // TODO(#60): generate static file routes at compile time 462 | STATIC_FILE_ROUTE("favicon.png", "image/png"); 463 | STATIC_FILE_ROUTE("index.js", "text/javascript"); 464 | STATIC_FILE_ROUTE("main.css", "text/css"); 465 | STATIC_FILE_ROUTE("reset.css", "text/css"); 466 | 467 | #undef STATIC_FILE_ROUTE 468 | } 469 | 470 | #undef STATIC_FOLDER 471 | return http_error(fd, 404, "Unknown path\n"); 472 | } 473 | 474 | #define MEMORY_CAPACITY (1 * MEGA) 475 | 476 | String mmap_file_to_string(const char *filepath) 477 | { 478 | int fd = open(filepath, O_RDONLY); 479 | if (fd < 0) { 480 | fprintf(stderr, "Cannot open file `%s'\n", filepath); 481 | abort(); 482 | } 483 | 484 | struct stat fd_stat; 485 | int err = fstat(fd, &fd_stat); 486 | assert(err == 0); 487 | 488 | String result; 489 | result.len = fd_stat.st_size; 490 | result.data = mmap(NULL, result.len, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); 491 | assert(result.data != MAP_FAILED); 492 | close(fd); 493 | 494 | return result; 495 | } 496 | 497 | void munmap_string(String s) 498 | { 499 | munmap((void*) s.data, s.len); 500 | } 501 | 502 | int main(int argc, char *argv[]) 503 | { 504 | if (argc < 3) { 505 | fprintf(stderr, "skedudle [address]\n"); 506 | exit(1); 507 | } 508 | 509 | const char *filepath = argv[1]; 510 | const char *port_cstr = argv[2]; 511 | const char *addr = "127.0.0.1"; 512 | if (argc >= 4) { 513 | addr = argv[3]; 514 | } 515 | 516 | Memory json_memory = { 517 | .capacity = MEMORY_CAPACITY, 518 | .buffer = malloc(MEMORY_CAPACITY) 519 | }; 520 | assert(json_memory.buffer); 521 | 522 | Memory request_memory = { 523 | .capacity = MEMORY_CAPACITY, 524 | .buffer = malloc(MEMORY_CAPACITY) 525 | }; 526 | assert(request_memory.buffer); 527 | 528 | String input = mmap_file_to_string(filepath); 529 | Json_Result result = parse_json_value(&json_memory, input); 530 | if (result.is_error) { 531 | print_json_error(stderr, result, input, filepath); 532 | exit(1); 533 | } 534 | printf("Parsing consumed %ld bytes of memory\n", json_memory.size); 535 | struct Schedule schedule = json_as_schedule(&json_memory, result.value); 536 | munmap_string(input); 537 | 538 | if (schedule.timezone.len == 0) { 539 | fprintf(stderr, "Timezone is not provided in the json file\n"); 540 | exit(1); 541 | } 542 | 543 | printf("Schedule timezone: %*.s\n", (int) schedule.timezone.len, schedule.timezone.data); 544 | 545 | char schedule_timezone[256]; 546 | snprintf(schedule_timezone, 256, ":%*.s", (int) schedule.timezone.len, schedule.timezone.data); 547 | setenv("TZ", schedule_timezone, 1); 548 | tzset(); 549 | 550 | uint16_t port = 0; 551 | 552 | { 553 | char *endptr; 554 | port = (uint16_t) strtoul(port_cstr, &endptr, 10); 555 | 556 | if (endptr == port_cstr) { 557 | fprintf(stderr, "%s is not a port number\n", port_cstr); 558 | exit(1); 559 | } 560 | } 561 | 562 | int server_fd = socket(AF_INET, SOCK_STREAM, 0); 563 | if (server_fd < 0) { 564 | fprintf(stderr, "Could not create socket epicly: %s\n", strerror(errno)); 565 | exit(1); 566 | } 567 | int option = 1; 568 | setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option)); 569 | 570 | struct sockaddr_in server_addr; 571 | memset(&server_addr, 0, sizeof(server_addr)); 572 | server_addr.sin_family = AF_INET; 573 | server_addr.sin_port = htons(port); 574 | server_addr.sin_addr.s_addr = inet_addr(addr); 575 | 576 | ssize_t err = bind(server_fd, (struct sockaddr*) &server_addr, sizeof(server_addr)); 577 | if (err != 0) { 578 | fprintf(stderr, "Could not bind socket epicly: %s\n", strerror(errno)); 579 | exit(1); 580 | } 581 | 582 | err = listen(server_fd, 69); 583 | if (err != 0) { 584 | fprintf(stderr, "Could not listen to socket, it's too quiet: %s\n", strerror(errno)); 585 | exit(1); 586 | } 587 | 588 | printf("[INFO] Listening to http://%s:%d/\n", addr, port); 589 | 590 | for (;;) { 591 | struct sockaddr_in client_addr; 592 | socklen_t client_addrlen = 0; 593 | int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addrlen); 594 | if (client_fd < 0) { 595 | fprintf(stderr, "Could not accept connection. This is unacceptable! %s\n", strerror(errno)); 596 | exit(1); 597 | } 598 | 599 | assert(client_addrlen == sizeof(client_addr)); 600 | 601 | // TODO(#57): running out of request memory should not crash the application 602 | handle_request(client_fd, &client_addr, &request_memory, &schedule); 603 | request_memory.size = 0; 604 | 605 | err = close(client_fd); 606 | if (err < 0) { 607 | fprintf(stderr, "Could not close client connection: %s\n", strerror(errno)); 608 | } 609 | } 610 | 611 | free(json_memory.buffer); 612 | free(request_memory.buffer); 613 | 614 | return 0; 615 | } 616 | -------------------------------------------------------------------------------- /src/json.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | 3 | #include 4 | #include 5 | #include 6 | #include "json.h" 7 | #include "utf8.h" 8 | 9 | Json_Value json_null = { .type = JSON_NULL }; 10 | Json_Value json_true = { .type = JSON_BOOLEAN, .boolean = 1 }; 11 | Json_Value json_false = { .type = JSON_BOOLEAN, .boolean = 0 }; 12 | 13 | Json_Value json_string(String string) 14 | { 15 | return (Json_Value) { 16 | .type = JSON_STRING, 17 | .string = string 18 | }; 19 | } 20 | 21 | static 22 | Json_Result parse_json_value_impl(Memory *memory, String source, int level); 23 | 24 | int json_isspace(char c) 25 | { 26 | return c == 0x20 || c == 0x0A || c == 0x0D || c == 0x09; 27 | } 28 | 29 | String json_trim_begin(String s) 30 | { 31 | while (s.len && json_isspace(*s.data)) { 32 | s.data++; 33 | s.len--; 34 | } 35 | return s; 36 | } 37 | 38 | void json_array_push(Memory *memory, Json_Array *array, Json_Value value) 39 | { 40 | assert(memory); 41 | assert(array); 42 | 43 | if (array->begin == NULL) { 44 | assert(array->end == NULL); 45 | array->begin = memory_alloc_aligned(memory, sizeof(Json_Array_Page), alignof(Json_Array_Page)); 46 | array->end = array->begin; 47 | memset(array->begin, 0, sizeof(Json_Array_Page)); 48 | } 49 | 50 | if (array->end->size >= JSON_ARRAY_PAGE_CAPACITY) { 51 | Json_Array_Page *next = memory_alloc_aligned(memory, sizeof(Json_Array_Page), alignof(Json_Array_Page)); 52 | memset(next, 0, sizeof(Json_Array_Page)); 53 | array->end->next = next; 54 | array->end = next; 55 | } 56 | 57 | assert(array->end->size < JSON_ARRAY_PAGE_CAPACITY); 58 | 59 | array->end->elements[array->end->size++] = value; 60 | } 61 | 62 | void json_object_push(Memory *memory, Json_Object *object, String key, Json_Value value) 63 | { 64 | assert(memory); 65 | assert(object); 66 | 67 | if (object->begin == NULL) { 68 | assert(object->end == NULL); 69 | object->begin = memory_alloc_aligned(memory, sizeof(Json_Object_Page), alignof(Json_Object_Page)); 70 | object->end = object->begin; 71 | memset(object->begin, 0, sizeof(Json_Object_Page)); 72 | } 73 | 74 | if (object->end->size >= JSON_OBJECT_PAGE_CAPACITY) { 75 | Json_Object_Page *next = memory_alloc_aligned(memory, sizeof(Json_Object_Page), alignof(Json_Object_Page)); 76 | memset(next, 0, sizeof(Json_Object_Page)); 77 | object->end->next = next; 78 | object->end = next; 79 | } 80 | 81 | assert(object->end->size < JSON_OBJECT_PAGE_CAPACITY); 82 | 83 | object->end->elements[object->end->size].key = key; 84 | object->end->elements[object->end->size].value = value; 85 | object->end->size += 1; 86 | } 87 | 88 | int64_t stoi64(String integer) 89 | { 90 | if (integer.len == 0) { 91 | return 0; 92 | } 93 | 94 | int64_t result = 0; 95 | int64_t sign = 1; 96 | 97 | if (*integer.data == '-') { 98 | sign = -1; 99 | chop(&integer, 1); 100 | } else if (*integer.data == '+') { 101 | sign = 1; 102 | chop(&integer, 1); 103 | } 104 | 105 | while (integer.len) { 106 | assert(isdigit(*integer.data)); 107 | result = result * 10 + (*integer.data - '0'); 108 | chop(&integer, 1); 109 | } 110 | 111 | return result * sign; 112 | } 113 | 114 | int64_t json_number_to_integer(Json_Number number) 115 | { 116 | int64_t exponent = stoi64(number.exponent); 117 | int64_t result = stoi64(number.integer); 118 | 119 | if (exponent > 0) { 120 | int64_t sign = result >= 0 ? 1 : -1; 121 | 122 | for (; exponent > 0; exponent -= 1) { 123 | int64_t x = 0; 124 | 125 | if (number.fraction.len) { 126 | x = *number.fraction.data - '0'; 127 | chop(&number.fraction, 1); 128 | } 129 | 130 | result = result * 10 + sign * x; 131 | } 132 | } 133 | 134 | for (; exponent < 0 && result; exponent += 1) { 135 | result /= 10; 136 | } 137 | 138 | return result; 139 | } 140 | 141 | static Json_Result parse_token(String source, String token, 142 | Json_Value value, 143 | const char *message) 144 | { 145 | if (string_equal(take(source, token.len), token)) { 146 | return (Json_Result) { 147 | .rest = drop(source, token.len), 148 | .value = value 149 | }; 150 | } 151 | 152 | return (Json_Result) { 153 | .is_error = 1, 154 | .message = message, 155 | .rest = source 156 | }; 157 | } 158 | 159 | static Json_Result parse_json_number(String source) 160 | { 161 | String integer = {0}; 162 | String fraction = {0}; 163 | String exponent = {0}; 164 | 165 | integer.data = source.data; 166 | 167 | if (source.len && *source.data == '-') { 168 | integer.len += 1; 169 | chop(&source, 1); 170 | } 171 | 172 | while (source.len && isdigit(*source.data)) { 173 | integer.len += 1; 174 | chop(&source, 1); 175 | } 176 | 177 | // TODO(#34): empty integer with fraction is not taken into account 178 | if (integer.len == 0 179 | || string_equal(integer, SLT("-")) 180 | || (integer.len > 1 && *integer.data == '0') 181 | || (integer.len > 2 && prefix_of(SLT("-0"), integer))) { 182 | return (Json_Result) { 183 | .is_error = 1, 184 | .rest = source, 185 | .message = "Incorrect number literal" 186 | }; 187 | } 188 | 189 | if (source.len && *source.data == '.') { 190 | chop(&source, 1); 191 | fraction.data = source.data; 192 | 193 | while (source.len && isdigit(*source.data)) { 194 | fraction.len += 1; 195 | chop(&source, 1); 196 | } 197 | 198 | if (fraction.len == 0) { 199 | return (Json_Result) { 200 | .is_error = 1, 201 | .rest = source, 202 | .message = "Incorrect number literal" 203 | }; 204 | } 205 | } 206 | 207 | if (source.len && tolower(*source.data) == 'e') { 208 | chop(&source, 1); 209 | 210 | exponent.data = source.data; 211 | 212 | if (source.len && (*source.data == '-' || *source.data == '+')) { 213 | exponent.len += 1; 214 | chop(&source, 1); 215 | } 216 | 217 | while (source.len && isdigit(*source.data)) { 218 | exponent.len += 1; 219 | chop(&source, 1); 220 | } 221 | 222 | if (exponent.len == 0 || 223 | string_equal(exponent, SLT("-")) || 224 | string_equal(exponent, SLT("+"))) { 225 | return (Json_Result) { 226 | .is_error = 1, 227 | .rest = source, 228 | .message = "Incorrect number literal" 229 | }; 230 | } 231 | } 232 | 233 | return (Json_Result) { 234 | .value = { 235 | .type = JSON_NUMBER, 236 | .number = { 237 | .integer = integer, 238 | .fraction = fraction, 239 | .exponent = exponent 240 | } 241 | }, 242 | .rest = source 243 | }; 244 | } 245 | 246 | static Json_Result parse_json_string_literal(String source) 247 | { 248 | if (source.len == 0 || *source.data != '"') { 249 | return (Json_Result) { 250 | .is_error = 1, 251 | .rest = source, 252 | .message = "Expected '\"'", 253 | }; 254 | } 255 | 256 | chop(&source, 1); 257 | 258 | String s = { 259 | .data = source.data, 260 | .len = 0 261 | }; 262 | 263 | while (source.len && *source.data != '"') { 264 | if (*source.data == '\\') { 265 | s.len++; 266 | chop(&source, 1); 267 | 268 | if (source.len == 0) { 269 | return (Json_Result) { 270 | .is_error = 1, 271 | .rest = source, 272 | .message = "Unfinished escape sequence", 273 | }; 274 | } 275 | } 276 | 277 | s.len++; 278 | chop(&source, 1); 279 | } 280 | 281 | if (source.len == 0) { 282 | return (Json_Result) { 283 | .is_error = 1, 284 | .rest = source, 285 | .message = "Expected '\"'", 286 | }; 287 | } 288 | 289 | chop(&source, 1); 290 | 291 | return (Json_Result) { 292 | .value = { 293 | .type = JSON_STRING, 294 | .string = s 295 | }, 296 | .rest = source 297 | }; 298 | } 299 | 300 | static int32_t unhex(char x) 301 | { 302 | x = tolower(x); 303 | 304 | if ('0' <= x && x <= '9') { 305 | return x - '0'; 306 | } else if ('a' <= x && x <= 'f') { 307 | return x - 'a' + 10; 308 | } 309 | 310 | return -1; 311 | } 312 | 313 | static Json_Result parse_escape_sequence(Memory *memory, String source) 314 | { 315 | static char unescape_map[][2] = { 316 | {'b', '\b'}, 317 | {'f', '\f'}, 318 | {'n', '\n'}, 319 | {'r', '\r'}, 320 | {'t', '\t'}, 321 | {'/', '/'}, 322 | {'\\', '\\'}, 323 | {'"', '"'}, 324 | }; 325 | static const size_t unescape_map_size = 326 | sizeof(unescape_map) / sizeof(unescape_map[0]); 327 | 328 | if (source.len == 0 || *source.data != '\\') { 329 | return (Json_Result) { 330 | .is_error = 1, 331 | .rest = source, 332 | .message = "Expected '\\'", 333 | }; 334 | } 335 | chop(&source, 1); 336 | 337 | if (source.len <= 0) { 338 | return (Json_Result) { 339 | .is_error = 1, 340 | .rest = source, 341 | .message = "Unfinished escape sequence", 342 | }; 343 | } 344 | 345 | for (size_t i = 0; i < unescape_map_size; ++i) { 346 | if (unescape_map[i][0] == *source.data) { 347 | return (Json_Result) { 348 | .rest = drop(source, 1), 349 | .value = { 350 | .type = JSON_STRING, 351 | .string = { 352 | .len = 1, 353 | .data = &unescape_map[i][1] 354 | } 355 | } 356 | }; 357 | } 358 | } 359 | 360 | if (*source.data != 'u') { 361 | return (Json_Result) { 362 | .is_error = 1, 363 | .rest = source, 364 | .message = "Unknown escape sequence" 365 | }; 366 | } 367 | chop(&source, 1); 368 | 369 | if (source.len < 4) { 370 | return (Json_Result) { 371 | .is_error = 1, 372 | .rest = source, 373 | .message = "Incomplete unicode point escape sequence" 374 | }; 375 | } 376 | 377 | uint32_t rune = 0; 378 | for (int i = 0; i < 4; ++i) { 379 | int32_t x = unhex(*source.data); 380 | if (x < 0) { 381 | return (Json_Result) { 382 | .is_error = 1, 383 | .rest = source, 384 | .message = "Incorrect hex digit" 385 | }; 386 | } 387 | rune = rune * 0x10 + x; 388 | chop(&source, 1); 389 | } 390 | 391 | if (0xD800 <= rune && rune <= 0xDBFF) { 392 | if (source.len < 6) { 393 | return (Json_Result) { 394 | .is_error = 1, 395 | .rest = source, 396 | .message = "Unfinished surrogate pair" 397 | }; 398 | } 399 | 400 | if (*source.data != '\\') { 401 | return (Json_Result) { 402 | .is_error = 1, 403 | .rest = source, 404 | .message = "Unfinished surrogate pair. Expected '\\'", 405 | }; 406 | } 407 | chop(&source, 1); 408 | 409 | if (*source.data != 'u') { 410 | return (Json_Result) { 411 | .is_error = 1, 412 | .rest = source, 413 | .message = "Unfinished surrogate pair. Expected 'u'", 414 | }; 415 | } 416 | chop(&source, 1); 417 | 418 | uint32_t surrogate = 0; 419 | for (int i = 0; i < 4; ++i) { 420 | int32_t x = unhex(*source.data); 421 | if (x < 0) { 422 | return (Json_Result) { 423 | .is_error = 1, 424 | .rest = source, 425 | .message = "Incorrect hex digit" 426 | }; 427 | } 428 | surrogate = surrogate * 0x10 + x; 429 | chop(&source, 1); 430 | } 431 | 432 | if (!(0xDC00 <= surrogate && surrogate <= 0xDFFF)) { 433 | return (Json_Result) { 434 | .is_error = 1, 435 | .rest = source, 436 | .message = "Invalid surrogate pair" 437 | }; 438 | } 439 | 440 | rune = 0x10000 + (((rune - 0xD800) << 10) |(surrogate - 0xDC00)); 441 | } 442 | 443 | if (rune > 0x10FFFF) { 444 | rune = 0xFFFD; 445 | } 446 | 447 | Utf8_Chunk utf8_chunk = utf8_encode_rune(rune); 448 | assert(utf8_chunk.size > 0); 449 | 450 | char *data = memory_alloc(memory, utf8_chunk.size); 451 | memcpy(data, utf8_chunk.buffer, utf8_chunk.size); 452 | 453 | return (Json_Result){ 454 | .value = { 455 | .type = JSON_STRING, 456 | .string = { 457 | .len = utf8_chunk.size, 458 | .data = data 459 | } 460 | }, 461 | .rest = source 462 | }; 463 | } 464 | 465 | static Json_Result parse_json_string(Memory *memory, String source) 466 | { 467 | Json_Result result = parse_json_string_literal(source); 468 | if (result.is_error) return result; 469 | assert(result.value.type == JSON_STRING); 470 | 471 | const size_t buffer_capacity = result.value.string.len; 472 | source = result.value.string; 473 | String rest = result.rest; 474 | 475 | char *buffer = memory_alloc(memory, buffer_capacity); 476 | size_t buffer_size = 0; 477 | 478 | while (source.len) { 479 | if (*source.data == '\\') { 480 | result = parse_escape_sequence(memory, source); 481 | if (result.is_error) return result; 482 | assert(result.value.type == JSON_STRING); 483 | assert(buffer_size + result.value.string.len <= buffer_capacity); 484 | memcpy(buffer + buffer_size, 485 | result.value.string.data, 486 | result.value.string.len); 487 | buffer_size += result.value.string.len; 488 | 489 | source = result.rest; 490 | } else { 491 | // TODO(#37): json parser is not aware of the input encoding 492 | assert(buffer_size < buffer_capacity); 493 | buffer[buffer_size++] = *source.data; 494 | chop(&source, 1); 495 | } 496 | } 497 | 498 | return (Json_Result) { 499 | .value = { 500 | .type = JSON_STRING, 501 | .string = { 502 | .data = buffer, 503 | .len = buffer_size 504 | }, 505 | }, 506 | .rest = rest 507 | }; 508 | } 509 | 510 | static Json_Result parse_json_array(Memory *memory, String source, int level) 511 | { 512 | assert(memory); 513 | 514 | if(source.len == 0 || *source.data != '[') { 515 | return (Json_Result) { 516 | .is_error = 1, 517 | .rest = source, 518 | .message = "Expected '['", 519 | }; 520 | } 521 | 522 | chop(&source, 1); 523 | 524 | source = json_trim_begin(source); 525 | 526 | if (source.len == 0) { 527 | return (Json_Result) { 528 | .is_error = 1, 529 | .rest = source, 530 | .message = "Expected ']'", 531 | }; 532 | } else if(*source.data == ']') { 533 | return (Json_Result) { 534 | .value = { .type = JSON_ARRAY }, 535 | .rest = drop(source, 1) 536 | }; 537 | } 538 | 539 | Json_Array array = {0}; 540 | 541 | while(source.len > 0) { 542 | Json_Result item_result = parse_json_value_impl(memory, source, level + 1); 543 | if(item_result.is_error) { 544 | return item_result; 545 | } 546 | 547 | json_array_push(memory, &array, item_result.value); 548 | 549 | source = json_trim_begin(item_result.rest); 550 | 551 | if (source.len == 0) { 552 | return (Json_Result) { 553 | .is_error = 1, 554 | .rest = source, 555 | .message = "Expected ']' or ','", 556 | }; 557 | } 558 | 559 | if (*source.data == ']') { 560 | return (Json_Result) { 561 | .value = { 562 | .type = JSON_ARRAY, 563 | .array = array 564 | }, 565 | .rest = drop(source, 1) 566 | }; 567 | } 568 | 569 | if (*source.data != ',') { 570 | return (Json_Result) { 571 | .is_error = 1, 572 | .rest = source, 573 | .message = "Expected ']' or ','", 574 | }; 575 | } 576 | 577 | source = json_trim_begin(drop(source, 1)); 578 | } 579 | 580 | return (Json_Result) { 581 | .is_error = 1, 582 | .rest = source, 583 | .message = "EOF", 584 | }; 585 | } 586 | 587 | static Json_Result parse_json_object(Memory *memory, String source, int level) 588 | { 589 | assert(memory); 590 | 591 | if (source.len == 0 || *source.data != '{') { 592 | return (Json_Result) { 593 | .is_error = 1, 594 | .rest = source, 595 | .message = "Expected '{'" 596 | }; 597 | } 598 | 599 | chop(&source, 1); 600 | 601 | source = json_trim_begin(source); 602 | 603 | if (source.len == 0) { 604 | return (Json_Result) { 605 | .is_error = 1, 606 | .rest = source, 607 | .message = "Expected '}'" 608 | }; 609 | } else if (*source.data == '}') { 610 | return (Json_Result) { 611 | .value = { .type = JSON_OBJECT }, 612 | .rest = drop(source, 1) 613 | }; 614 | } 615 | 616 | Json_Object object = {0}; 617 | 618 | while (source.len > 0) { 619 | source = json_trim_begin(source); 620 | 621 | Json_Result key_result = parse_json_string(memory, source); 622 | if (key_result.is_error) { 623 | return key_result; 624 | } 625 | source = json_trim_begin(key_result.rest); 626 | 627 | if (source.len == 0 || *source.data != ':') { 628 | return (Json_Result) { 629 | .is_error = 1, 630 | .rest = source, 631 | .message = "Expected ':'" 632 | }; 633 | } 634 | 635 | chop(&source, 1); 636 | 637 | Json_Result value_result = parse_json_value_impl(memory, source, level + 1); 638 | if (value_result.is_error) { 639 | return value_result; 640 | } 641 | source = json_trim_begin(value_result.rest); 642 | 643 | assert(key_result.value.type == JSON_STRING); 644 | json_object_push(memory, &object, key_result.value.string, value_result.value); 645 | 646 | if (source.len == 0) { 647 | return (Json_Result) { 648 | .is_error = 1, 649 | .rest = source, 650 | .message = "Expected '}' or ','", 651 | }; 652 | } 653 | 654 | if (*source.data == '}') { 655 | return (Json_Result) { 656 | .value = { 657 | .type = JSON_OBJECT, 658 | .object = object 659 | }, 660 | .rest = drop(source, 1) 661 | }; 662 | } 663 | 664 | if (*source.data != ',') { 665 | return (Json_Result) { 666 | .is_error = 1, 667 | .rest = source, 668 | .message = "Expected '}' or ','", 669 | }; 670 | } 671 | 672 | source = drop(source, 1); 673 | } 674 | 675 | return (Json_Result) { 676 | .is_error = 1, 677 | .rest = source, 678 | .message = "EOF", 679 | }; 680 | } 681 | 682 | static 683 | Json_Result parse_json_value_impl(Memory *memory, String source, int level) 684 | { 685 | if (level >= JSON_DEPTH_MAX_LIMIT) { 686 | return (Json_Result) { 687 | .is_error = 1, 688 | .message = "Reach the max limit of depth", 689 | .rest = source 690 | }; 691 | } 692 | 693 | source = json_trim_begin(source); 694 | 695 | if (source.len == 0) { 696 | return (Json_Result) { 697 | .is_error = 1, 698 | .message = "EOF", 699 | .rest = source 700 | }; 701 | } 702 | 703 | switch (*source.data) { 704 | case 'n': return parse_token(source, SLT("null"), json_null, "Expected `null`"); 705 | case 't': return parse_token(source, SLT("true"), json_true, "Expected `true`"); 706 | case 'f': return parse_token(source, SLT("false"), json_false, "Expected `false`"); 707 | case '"': return parse_json_string(memory, source); 708 | case '[': return parse_json_array(memory, source, level); 709 | case '{': return parse_json_object(memory, source, level); 710 | } 711 | 712 | return parse_json_number(source); 713 | } 714 | 715 | Json_Result parse_json_value(Memory *memory, String source) 716 | { 717 | return parse_json_value_impl(memory, source, 0); 718 | } 719 | 720 | static 721 | void print_json_null(FILE *stream) 722 | { 723 | fprintf(stream, "null"); 724 | } 725 | 726 | static 727 | void print_json_boolean(FILE *stream, int boolean) 728 | { 729 | if (boolean) { 730 | fprintf(stream, "true"); 731 | } else { 732 | fprintf(stream, "false"); 733 | } 734 | } 735 | 736 | static 737 | void print_json_number(FILE *stream, Json_Number number) 738 | { 739 | fwrite(number.integer.data, 1, number.integer.len, stream); 740 | 741 | if (number.fraction.len > 0) { 742 | fputc('.', stream); 743 | fwrite(number.fraction.data, 1, number.fraction.len, stream); 744 | } 745 | 746 | if (number.exponent.len > 0) { 747 | fputc('e', stream); 748 | fwrite(number.exponent.data, 1, number.exponent.len, stream); 749 | } 750 | } 751 | 752 | static 753 | int json_get_utf8_char_len(unsigned char ch) { 754 | if ((ch & 0x80) == 0) return 1; 755 | switch (ch & 0xf0) { 756 | case 0xf0: 757 | return 4; 758 | case 0xe0: 759 | return 3; 760 | default: 761 | return 2; 762 | } 763 | } 764 | 765 | static 766 | void print_json_string(FILE *stream, String string) 767 | { 768 | const char *hex_digits = "0123456789abcdef"; 769 | const char *specials = "btnvfr"; 770 | const char *p = string.data; 771 | 772 | fputc('"', stream); 773 | size_t cl; 774 | for (size_t i = 0; i < string.len; i++) { 775 | unsigned char ch = ((unsigned char *) p)[i]; 776 | if (ch == '"' || ch == '\\') { 777 | fwrite("\\", 1, 1, stream); 778 | fwrite(p + i, 1, 1, stream); 779 | } else if (ch >= '\b' && ch <= '\r') { 780 | fwrite("\\", 1, 1, stream); 781 | fwrite(&specials[ch - '\b'], 1, 1, stream); 782 | } else if (isprint(ch)) { 783 | fwrite(p + i, 1, 1, stream); 784 | } else if ((cl = json_get_utf8_char_len(ch)) == 1) { 785 | fwrite("\\u00", 1, 4, stream); 786 | fwrite(&hex_digits[(ch >> 4) % 0xf], 1, 1, stream); 787 | fwrite(&hex_digits[ch % 0xf], 1, 1, stream); 788 | } else { 789 | fwrite(p + i, 1, cl, stream); 790 | i += cl - 1; 791 | } 792 | } 793 | fputc('"', stream); 794 | } 795 | 796 | static 797 | void print_json_array(FILE *stream, Json_Array array) 798 | { 799 | fprintf(stream, "["); 800 | int t = 0; 801 | for (Json_Array_Page *page = array.begin; page != NULL; page = page->next) { 802 | for (size_t i = 0; i < page->size; ++i) { 803 | if (t) { 804 | printf(","); 805 | } else { 806 | t = 1; 807 | } 808 | print_json_value(stream, page->elements[i]); 809 | } 810 | } 811 | fprintf(stream, "]"); 812 | } 813 | 814 | void print_json_object(FILE *stream, Json_Object object) 815 | { 816 | fprintf(stream, "{"); 817 | int t = 0; 818 | for (Json_Object_Page *page = object.begin; page != NULL; page = page->next) { 819 | for (size_t i = 0; i < page->size; ++i) { 820 | if (t) { 821 | printf(","); 822 | } else { 823 | t = 1; 824 | } 825 | print_json_string(stream, page->elements[i].key); 826 | fprintf(stream, ":"); 827 | print_json_value(stream, page->elements[i].value); 828 | } 829 | } 830 | fprintf(stream, "}"); 831 | } 832 | 833 | void print_json_value(FILE *stream, Json_Value value) 834 | { 835 | switch (value.type) { 836 | case JSON_NULL: { 837 | print_json_null(stream); 838 | } break; 839 | case JSON_BOOLEAN: { 840 | print_json_boolean(stream, value.boolean); 841 | } break; 842 | case JSON_NUMBER: { 843 | print_json_number(stream, value.number); 844 | } break; 845 | case JSON_STRING: { 846 | print_json_string(stream, value.string); 847 | } break; 848 | case JSON_ARRAY: { 849 | print_json_array(stream, value.array); 850 | } break; 851 | case JSON_OBJECT: { 852 | print_json_object(stream, value.object); 853 | } break; 854 | } 855 | } 856 | 857 | void print_json_error(FILE *stream, Json_Result result, 858 | String source, const char *prefix) 859 | { 860 | assert(stream); 861 | assert(source.data <= result.rest.data); 862 | 863 | size_t n = result.rest.data - source.data; 864 | 865 | for (size_t line_number = 1; source.len; ++line_number) { 866 | String line = chop_line(&source); 867 | 868 | if (n <= line.len) { 869 | fprintf(stream, "%s:%ld: %s\n", prefix, line_number, result.message); 870 | fwrite(line.data, 1, line.len, stream); 871 | fputc('\n', stream); 872 | 873 | for (size_t j = 0; j < n; ++j) { 874 | fputc(' ', stream); 875 | } 876 | fputc('^', stream); 877 | fputc('\n', stream); 878 | break; 879 | } 880 | 881 | n -= line.len + 1; 882 | } 883 | 884 | for (int i = 0; source.len && i < 3; ++i) { 885 | String line = chop_line(&source); 886 | fwrite(line.data, 1, line.len, stream); 887 | fputc('\n', stream); 888 | } 889 | } 890 | 891 | static 892 | void print_json_number_fd(int fd, Json_Number number) 893 | { 894 | write(fd, number.integer.data, number.integer.len); 895 | 896 | if (number.fraction.len > 0) { 897 | write(fd, ".", 1); 898 | write(fd, number.fraction.data, number.fraction.len); 899 | } 900 | 901 | if (number.exponent.len > 0) { 902 | write(fd, "e", 1); 903 | write(fd, number.exponent.data, number.exponent.len); 904 | } 905 | } 906 | 907 | static 908 | void print_json_string_fd(int fd, String string) 909 | { 910 | const char *hex_digits = "0123456789abcdef"; 911 | const char *specials = "btnvfr"; 912 | const char *p = string.data; 913 | 914 | write(fd, "\"", 1); 915 | size_t cl; 916 | for (size_t i = 0; i < string.len; i++) { 917 | unsigned char ch = ((unsigned char *) p)[i]; 918 | if (ch == '"' || ch == '\\') { 919 | write(fd, "\\", 1); 920 | write(fd, p + i, 1); 921 | } else if (ch >= '\b' && ch <= '\r') { 922 | write(fd, "\\", 1); 923 | write(fd, &specials[ch - '\b'], 1); 924 | } else if (isprint(ch)) { 925 | write(fd, p + i, 1); 926 | } else if ((cl = json_get_utf8_char_len(ch)) == 1) { 927 | write(fd, "\\u00", 4); 928 | write(fd, &hex_digits[(ch >> 4) % 0xf], 1); 929 | write(fd, &hex_digits[ch % 0xf], 1); 930 | } else { 931 | write(fd, p + i, cl); 932 | i += cl - 1; 933 | } 934 | } 935 | write(fd, "\"", 1); 936 | } 937 | 938 | static 939 | void print_json_array_fd(int fd, Json_Array array) 940 | { 941 | dprintf(fd, "["); 942 | int t = 0; 943 | for (Json_Array_Page *page = array.begin; page != NULL; page = page->next) { 944 | for (size_t i = 0; i < page->size; ++i) { 945 | if (t) { 946 | dprintf(fd, ","); 947 | } else { 948 | t = 1; 949 | } 950 | print_json_value_fd(fd, page->elements[i]); 951 | } 952 | } 953 | dprintf(fd, "]"); 954 | } 955 | 956 | void print_json_object_fd(int fd, Json_Object object) 957 | { 958 | dprintf(fd, "{"); 959 | int t = 0; 960 | for (Json_Object_Page *page = object.begin; page != NULL; page = page->next) { 961 | for (size_t i = 0; i < page->size; ++i) { 962 | if (t) { 963 | dprintf(fd, ","); 964 | } else { 965 | t = 1; 966 | } 967 | print_json_string_fd(fd, page->elements[i].key); 968 | dprintf(fd, ":"); 969 | print_json_value_fd(fd, page->elements[i].value); 970 | } 971 | } 972 | dprintf(fd, "}"); 973 | } 974 | 975 | void print_json_value_fd(int fd, Json_Value value) 976 | { 977 | switch (value.type) { 978 | case JSON_NULL: { 979 | dprintf(fd, "null"); 980 | } break; 981 | case JSON_BOOLEAN: { 982 | dprintf(fd, value.boolean ? "true" : "false"); 983 | } break; 984 | case JSON_NUMBER: { 985 | print_json_number_fd(fd, value.number); 986 | } break; 987 | case JSON_STRING: { 988 | print_json_string_fd(fd, value.string); 989 | } break; 990 | case JSON_ARRAY: { 991 | print_json_array_fd(fd, value.array); 992 | } break; 993 | case JSON_OBJECT: { 994 | print_json_object_fd(fd, value.object); 995 | } break; 996 | } 997 | } 998 | --------------------------------------------------------------------------------