1 #include "chatlog.h"
2 
3 #include "filesys.h"
4 // TODO including native.h files should never be needed, refactor filesys.h to provide necessary API
5 #include "debug.h"
6 #include "messages.h"
7 #include "text.h"
8 
9 #include "native/filesys.h"
10 
11 #include <stdint.h>
12 #include <stdlib.h>
13 
chatlog_get_file(char hex[TOX_PUBLIC_KEY_SIZE * 2],bool append)14 static FILE* chatlog_get_file(char hex[TOX_PUBLIC_KEY_SIZE * 2], bool append) {
15     char name[TOX_PUBLIC_KEY_SIZE * 2 + sizeof(".new.txt")];
16     snprintf(name, sizeof(name), "%.*s.new.txt", TOX_PUBLIC_KEY_SIZE * 2, hex);
17 
18     FILE *file;
19     if (append) {
20         file = utox_get_file(name, NULL, UTOX_FILE_OPTS_READ | UTOX_FILE_OPTS_WRITE | UTOX_FILE_OPTS_MKDIR);
21         if (!file) {
22             return NULL;
23         }
24 
25         fseek(file, 0, SEEK_END);
26     } else {
27         file = utox_get_file(name, NULL, UTOX_FILE_OPTS_READ);
28     }
29 
30     return file;
31 }
32 
utox_save_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2],uint8_t * data,size_t length)33 size_t utox_save_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2], uint8_t *data, size_t length) {
34     FILE *fp = chatlog_get_file(hex, true);
35     if (!fp) {
36         LOG_ERR("uTox", "Error getting a file handle for this chatlog!");
37         return 0;
38     }
39     // Seek to the beginning of the file first because grayhatter has had issues with this on Windows.
40     // (and he really doesn't want uTox eating people's chat logs)
41     fseeko(fp, 0, SEEK_SET);
42     fseeko(fp, 0, SEEK_END);
43     off_t offset = ftello(fp);
44     fwrite(data, length, 1, fp);
45     fclose(fp);
46 
47     return offset;
48 }
49 
utox_count_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2])50 static size_t utox_count_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2]) {
51     FILE *file = chatlog_get_file(hex, false);
52 
53     if (!file) {
54         return 0;
55     }
56 
57     LOG_FILE_MSG_HEADER header;
58     size_t records_count = 0;
59 
60     while (fread(&header, sizeof(header), 1, file) == 1) {
61         fseeko(file, header.author_length + header.msg_length + 1, SEEK_CUR);
62         records_count++;
63     }
64 
65 
66     if (ferror(file) || !feof(file)) {
67         /* TODO: consider removing or truncating the log file.
68          * If !feof() this means that the file has an incomplete record,
69          * which would prevent it from loading forever, even though
70          * new records will keep being appended as usual. */
71         LOG_ERR("Chatlog", "Log read err; trying to count history for friend %.*s", TOX_PUBLIC_KEY_SIZE * 2, hex);
72         fclose(file);
73         return 0;
74     }
75 
76     fclose(file);
77     return records_count;
78 }
79 
80 /* TODO create fxn that will try to recover a corrupt chat history.
81  *
82  * In the majority of bug reports the corrupt message is often the first, so in
83  * theory we should be able to trim the start of the chatlog up to and including
84  * the first \n char. We may have to do so multiple times, but once we find the
85  * first valid message everything else should "work" */
utox_load_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2],size_t * size,uint32_t count,uint32_t skip)86 MSG_HEADER **utox_load_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2], size_t *size, uint32_t count, uint32_t skip) {
87     /* Because every platform is different, we have to ask them to open the file for us.
88      * However once we have it, every platform does the same thing, this should prevent issues
89      * from occurring on a single platform. */
90 
91     size_t records_count = utox_count_chatlog(hex);
92     if (skip >= records_count) {
93         if (skip > 0) {
94             LOG_ERR("Chatlog", "Error, skipped all records");
95         } else {
96             LOG_INFO("Chatlog", "No log exists.");
97         }
98         return NULL;
99     }
100 
101     FILE *file = chatlog_get_file(hex, false);
102     if (!file) {
103         LOG_TRACE("Chatlog", "Log read:\tUnable to access file provided.");
104         return NULL;
105     }
106 
107     if (count > (records_count - skip)) {
108         count = records_count - skip;
109     }
110 
111     MSG_HEADER **data = calloc(count + 1, sizeof(MSG_HEADER *));
112     MSG_HEADER **start = data;
113 
114     if (!data) {
115         LOG_ERR("Chatlog", "Log read:\tCouldn't allocate memory for log entries.");
116         fclose(file);
117         return NULL;
118     }
119 
120     size_t start_at = records_count - count - skip;
121     size_t actual_count = 0;
122 
123     size_t file_offset = 0;
124 
125     LOG_FILE_MSG_HEADER header;
126     while (fread(&header, sizeof(header), 1, file) == 1) {
127         if (start_at) {
128             fseeko(file, header.author_length, SEEK_CUR); /* Skip the recorded author */
129             fseeko(file, header.msg_length, SEEK_CUR);    /* Skip the message */
130             fseeko(file, 1, SEEK_CUR);                    /* Skip the newline char */
131             start_at--;
132             file_offset = ftello(file);
133             continue;
134         }
135 
136         if (count) {
137             /* we have to skip the author name for now, it's left here for group chats support in the future */
138             fseeko(file, header.author_length, SEEK_CUR);
139             if (header.msg_length > 1 << 16) {
140                 LOG_ERR("Chatlog", "Can't malloc that much, you'll probably have to move or delete your"
141                             " history for this peer.\n\t\tFriend number %.*s, count %u,"
142                             " actual_count %lu, start at %lu, error size %lu.\n",
143                             TOX_PUBLIC_KEY_SIZE * 2, hex, count, actual_count, start_at, header.msg_length);
144                 if (size) {
145                     *size = 0;
146                 }
147 
148                 fclose(file);
149                 return start;
150             }
151 
152             MSG_HEADER *msg = calloc(1, sizeof(MSG_HEADER));
153             if (!msg) {
154                 LOG_ERR("Chatlog", "Unable to malloc... sorry!");
155                 free(start);
156                 fclose(file);
157                 return NULL;
158             }
159 
160             msg->our_msg       = header.author;
161             msg->receipt_time  = header.receipt;
162             msg->time          = header.time;
163             msg->msg_type      = header.msg_type;
164             msg->disk_offset   = file_offset;
165 
166             msg->via.txt.length        = header.msg_length;
167             msg->via.txt.msg = calloc(1, msg->via.txt.length);
168             if (!msg->via.txt.msg) {
169                 LOG_ERR("Chatlog", "Unable to malloc for via.txt.msg... sorry!");
170                 free(start);
171                 free(msg);
172                 fclose(file);
173                 return NULL;
174             }
175 
176             msg->via.txt.author_length = header.author_length;
177             // TODO: msg->via.txt.author used to be allocated but left empty. Commented out for now.
178             // msg->via.txt.author = calloc(1, msg->via.txt.author_length);
179             // if (!msg->via.txt.author) {
180             //     LOG_ERR("Chatlog", "Unable to malloc for via.txt.author... sorry!");
181             //     free(msg->via.txt.msg);
182             //     free(msg);
183             //     fclose(file);
184             //     return NULL;
185             // }
186 
187             if (fread(msg->via.txt.msg, msg->via.txt.length, 1, file) != 1) {
188                 LOG_ERR("Chatlog", "Log read:\tError reading record %u of length %u at offset %lu: stopping.",
189                             count, msg->via.txt.length, msg->disk_offset);
190                 // free(msg->via.txt.author);
191                 free(msg->via.txt.msg);
192                 free(msg);
193                 break;
194             }
195 
196             msg->via.txt.length = utf8_validate((uint8_t *)msg->via.txt.msg, msg->via.txt.length);
197             *data++ = msg;
198             --count;
199             ++actual_count;
200             fseeko(file, 1, SEEK_CUR); /* seek an extra \n char */
201             file_offset = ftello(file);
202         }
203     }
204 
205     fclose(file);
206 
207     if (size) {
208         *size = actual_count;
209     }
210 
211     return start;
212 }
213 
utox_update_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2],size_t offset,uint8_t * data,size_t length)214 bool utox_update_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2], size_t offset, uint8_t *data, size_t length) {
215     FILE *file = chatlog_get_file(hex, true);
216 
217     if (!file) {
218         LOG_ERR("History", "Unable to access file provided.");
219         return false;
220     }
221 
222     if (fseeko(file, offset, SEEK_SET)) {
223         LOG_ERR("Chatlog", "History:\tUnable to seek to position %lu in file provided.", offset);
224         fclose(file);
225         return false;
226     }
227 
228     fwrite(data, length, 1, file);
229     fclose(file);
230 
231     return true;
232 }
233 
utox_remove_friend_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2])234 bool utox_remove_friend_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2]) {
235     char name[TOX_PUBLIC_KEY_SIZE * 2 + sizeof(".new.txt")];
236 
237     snprintf(name, sizeof(name), "%.*s.new.txt", TOX_PUBLIC_KEY_SIZE * 2, hex);
238 
239     return utox_remove_file((uint8_t*)name, sizeof(name));
240 }
241 
utox_export_chatlog_init(uint32_t friend_number)242 void utox_export_chatlog_init(uint32_t friend_number) {
243     native_export_chatlog_init(friend_number);
244 }
245 
utox_export_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2],FILE * dest_file)246 void utox_export_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2], FILE *dest_file) {
247     if (!dest_file) {
248         return;
249     }
250 
251     LOG_FILE_MSG_HEADER header;
252     FILE *file = chatlog_get_file(hex, false);
253 
254     struct tm *tm_curr;
255     struct tm_tmp {
256         int tm_year;
257         int tm_mon;
258         int tm_mday;
259     } tm_prev = { .tm_mday = 1};
260 
261     while (fread(&header, sizeof(header), 1, file) == 1) {
262         tm_curr = localtime(&header.time);
263 
264         if (tm_curr->tm_year > tm_prev.tm_year
265             || (tm_curr->tm_year == tm_prev.tm_year && tm_curr->tm_mon > tm_prev.tm_mon)
266             || (tm_curr->tm_year == tm_prev.tm_year && tm_curr->tm_mon == tm_prev.tm_mon && tm_curr->tm_mday > tm_prev.tm_mday))
267         {
268             char buffer[128];
269             size_t len = strftime(buffer, 128,  "Day has changed to %A %B %d %Y\n", tm_curr);
270             fwrite(buffer, len, 1, dest_file);
271         }
272 
273         /* Write Timestamp */
274         fprintf(dest_file, "[%02d:%02d]", tm_curr->tm_hour, tm_curr->tm_min);
275         tm_prev.tm_year = tm_curr->tm_year;
276         tm_prev.tm_mon = tm_curr->tm_mon;
277         tm_prev.tm_mday = tm_curr->tm_mday;
278 
279         int c;
280         if (header.msg_type == MSG_TYPE_NOTICE) {
281             fseek(file, header.author_length, SEEK_CUR);
282         } else {
283             /* Write Author */
284             fwrite(" <", 2, 1, dest_file);
285             for (size_t i = 0; i < header.author_length; ++i) {
286                 c = fgetc(file);
287                 if (c != EOF) {
288                     fputc(c, dest_file);
289                 }
290             }
291             fwrite(">", 1, 1, dest_file);
292         }
293 
294         /* Write text */
295         fwrite(" ", 1, 1, dest_file);
296         for (size_t i = 0; i < header.msg_length; ++i) {
297             c = fgetc(file);
298             if (c != EOF) {
299                 fputc(c, dest_file);
300             }
301         }
302         c = fgetc(file); /* the newline char */
303         fputc(c, dest_file);
304     }
305 
306     fclose(file);
307     fclose(dest_file);
308 }
309