format.c (7320B)
1 #include "format.h" 2 3 #include <err.h> 4 #include <stdlib.h> 5 #include <string.h> 6 #include <time.h> 7 8 void print_time(FILE* out, time_t time, int timezone_offset) { 9 // Reject any offset > 24 hours. 10 if (timezone_offset < -1440 || timezone_offset > 1440) { 11 warnx("invalid timezone offset: %d", timezone_offset); 12 return; 13 } 14 time_t local_time = time + (timezone_offset * 60); 15 struct tm* time_in = gmtime(&local_time); 16 if (!time_in) { 17 return; 18 } 19 20 char formatted_time[32]; 21 if (!strftime(formatted_time, sizeof(formatted_time), "%a, %e %b %Y %H:%M:%S", 22 time_in)) { 23 err(1, "strftime"); 24 } 25 26 char timezone_sign = timezone_offset < 0 ? '-' : '+'; 27 int timezone_hours = (timezone_offset < 0 ? -1 : 1) * timezone_offset / 60; 28 int timezone_mins = (timezone_offset < 0 ? -1 : 1) * timezone_offset % 60; 29 char out_str[64]; 30 if (snprintf(out_str, sizeof(out_str), "%s %c%02d%02d", formatted_time, 31 timezone_sign, timezone_hours, timezone_mins) < 0) { 32 err(1, "snprintf"); 33 } 34 fprintf(out, "%s", out_str); 35 } 36 37 void print_time_z(FILE* out, time_t time) { 38 struct tm* time_in = gmtime(&time); 39 if (!time_in) { 40 return; 41 } 42 43 char formatted_time[32]; 44 if (!strftime(formatted_time, sizeof(formatted_time), "%Y-%m-%dT%H:%M:%SZ", 45 time_in)) { 46 err(1, "strftime"); 47 } 48 fprintf(out, "%s", formatted_time); 49 } 50 51 void print_time_short(FILE* out, time_t time) { 52 struct tm* time_in = gmtime(&time); 53 if (!time_in) { 54 return; 55 } 56 57 char formatted_time[32]; 58 if (!strftime(formatted_time, sizeof(formatted_time), "%Y-%m-%d %H:%M", 59 time_in)) { 60 err(1, "strftime"); 61 } 62 fprintf(out, "%s", formatted_time); 63 } 64 65 void print_percent_encoded(FILE* out, const char* str) { 66 static const char* hex_chars = "0123456789ABCDEF"; 67 68 size_t str_len = strlen(str); 69 for (size_t i = 0; i < str_len; i++) { 70 unsigned char uc = str[i]; 71 // NOTE: do not encode '/' for paths or ",-." 72 if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') || uc == '[' || 73 uc == ']') { 74 fprintf(out, "%%%c%c", hex_chars[(uc >> 4) & 0x0f], hex_chars[uc & 0x0f]); 75 } else { 76 fprintf(out, "%c", uc); 77 } 78 } 79 } 80 81 void print_xml_encoded(FILE* out, const char* str) { 82 print_xml_encoded_len(out, str, -1, true); 83 } 84 85 void print_xml_encoded_len(FILE* out, 86 const char* str, 87 ssize_t str_len, 88 bool output_crlf) { 89 size_t len = (str_len >= 0) ? (size_t)str_len : strlen(str); 90 for (size_t i = 0; i < len && str[i] != '\0'; i++) { 91 char c = str[i]; 92 switch (c) { 93 case '<': 94 fprintf(out, "<"); 95 break; 96 case '>': 97 fprintf(out, ">"); 98 break; 99 case '\'': 100 fprintf(out, "'"); 101 break; 102 case '&': 103 fprintf(out, "&"); 104 break; 105 case '"': 106 fprintf(out, """); 107 break; 108 case '\r': 109 case '\n': 110 if (output_crlf) { 111 fprintf(out, "%c", c); 112 } 113 break; 114 default: 115 fprintf(out, "%c", c); 116 } 117 } 118 } 119 120 void print_gopher_text(FILE* out, const char* str, bool output_lf) { 121 print_gopher_text_len(out, str, -1, output_lf); 122 } 123 124 void print_gopher_text_len(FILE* out, 125 const char* str, 126 ssize_t str_len, 127 bool output_lf) { 128 size_t len = (str_len >= 0) ? (size_t)str_len : strlen(str); 129 bool start_of_line = true; 130 for (size_t i = 0; i < len && str[i] != '\0'; i++) { 131 char c = str[i]; 132 if (output_lf && start_of_line && c == '[') { 133 fprintf(out, "[|"); 134 } else if (c == '\t') { 135 fprintf(out, " "); 136 } else if (c == '\r' || (!output_lf && c == '\n')) { 137 /* Ignore. */ 138 } else { 139 fprintf(out, "%c", c); 140 } 141 start_of_line = (c == '\n'); 142 } 143 } 144 145 void print_gopher_link(FILE* out, const char* str) { 146 for (size_t i = 0; str[i] != '\0'; i++) { 147 char c = str[i]; 148 if (c == '|') { 149 fprintf(out, "\\|"); 150 } else if (c == '\t') { 151 fprintf(out, " "); 152 } else if (c == '\r' || c == '\n') { 153 // Ignore. 154 } else { 155 fprintf(out, "%c", c); 156 } 157 } 158 } 159 160 static size_t utf8_char_length(const char* str) { 161 unsigned char c = (unsigned char)*str; 162 if (c < 0x80) { 163 return 1; 164 } 165 if ((c & 0xe0) == 0xc0) { 166 return 2; 167 } 168 if ((c & 0xf0) == 0xe0) { 169 return 3; 170 } 171 if ((c & 0xf8) == 0xf0) { 172 return 4; 173 } 174 // Invalid UTF-8 sequence. Treat as single byte. 175 return 1; 176 } 177 178 static bool is_zwj(const char* str) { 179 return str[0] == '\xe2' && str[1] == '\x80' && str[2] == '\x8d'; 180 } 181 182 static bool is_variation_selector(const char* str) { 183 return str[0] == '\xef' && str[1] == '\xb8' && str[2] == '\x8f'; 184 } 185 186 static size_t get_char_width(wchar_t c) { 187 // ASCII characters in the range [0-31] and [127-255] are control characters 188 // or non-printable characters 189 if ((c >= 0 && c <= 31) || (c >= 127 && c <= 255)) { 190 return 1; 191 } 192 193 // Emoji range. 194 if (c >= 0x1F300 && c <= 0x1F6FF) { 195 return 2; 196 } 197 198 // Extended pictographic characters. 199 if (c >= 0x1F900 && c <= 0x1F9FF) { 200 return 2; 201 } 202 203 // Hiragana and Katakana ranges. 204 if ((c >= 0x3040 && c <= 0x309F) || (c >= 0x30A0 && c <= 0x30FF)) { 205 return 2; 206 } 207 208 // Kanji range. 209 if (c >= 0x4E00 && c <= 0x9FFF) { 210 return 2; 211 } 212 return 1; 213 } 214 215 // TODO(cbracken): There has to be a better way. 216 void print_gopher_link_padded(FILE* out, 217 const char* str, 218 size_t width, 219 char pad_char) { 220 size_t str_len = strlen(str); 221 size_t char_count = 0; 222 size_t last_char_width = 0; 223 size_t byte_count = 0; 224 while (byte_count < str_len) { 225 size_t pos = byte_count; 226 size_t char_length = utf8_char_length(&str[byte_count]); 227 byte_count += char_length; 228 if (char_count == width - 1 && byte_count < str_len) { 229 fprintf(out, "\u2026"); 230 last_char_width = 1; 231 char_count += last_char_width; 232 } else if (char_count < width) { 233 if (str[pos] == '|') { 234 fprintf(out, "\\|"); 235 char_count++; 236 last_char_width = 1; 237 } else if (str[pos] == '\t') { 238 for (size_t i = 0; i < 8 && char_count < width; i++) { 239 fprintf(out, " "); 240 char_count++; 241 } 242 last_char_width = 1; 243 } else if (str[pos] == '\r' || str[pos] == '\n') { 244 // Ignore. 245 last_char_width = 0; 246 } else if (char_length == 3 && is_zwj(&str[pos])) { 247 fprintf(out, "\xe2\x80\x8d"); 248 char_count -= last_char_width; 249 last_char_width = 0; 250 } else if (char_length == 3 && is_variation_selector(&str[pos])) { 251 fprintf(out, "\xef\xb8\x8f"); 252 char_count -= last_char_width; 253 last_char_width = 0; 254 } else { 255 for (size_t i = pos; i < byte_count; i++) { 256 fprintf(out, "%c", str[i]); 257 } 258 wchar_t wide_char; 259 if (mbtowc(&wide_char, &str[pos], MB_CUR_MAX) >= 0) { 260 last_char_width = get_char_width(wide_char); 261 } else { 262 last_char_width = 1; 263 } 264 char_count += last_char_width; 265 } 266 } 267 } 268 if (pad_char != '\0') { 269 while (char_count < width) { 270 fprintf(out, "%c", pad_char); 271 char_count++; 272 } 273 } 274 }