date.c: Remove a bunch of undesired code.
[notmuch] / message.c
1 /* message.c - Utility functions for parsing an email message for notmuch.
2  *
3  * Copyright © 2009 Carl Worth
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see http://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include <stdarg.h>
22
23 #include "notmuch-private.h"
24
25 #include <glib.h>
26
27 struct _notmuch_message {
28     /* File objects */
29     int fd;
30     void *map;
31
32     /* Header storage */
33     int restrict_headers;
34     GHashTable *headers;
35
36     /* Parsing state */
37     char *start;
38     size_t size;
39     const char *next_line;
40     int parsing_started;
41     int parsing_finished;
42 };
43
44 static int
45 strcase_equal (const void *a, const void *b)
46 {
47     return strcasecmp (a, b) == 0;
48 }
49
50 static unsigned int
51 strcase_hash (const void *ptr)
52 {
53     const char *s = ptr;
54
55     /* This is the djb2 hash. */
56     unsigned int hash = 5381;
57     while (s && *s) {
58         hash = ((hash << 5) + hash) + tolower (*s);
59         s++;
60     }
61
62     return hash;
63 }
64
65 notmuch_message_t *
66 notmuch_message_open (const char *filename)
67 {
68     notmuch_message_t *message;
69     struct stat st;
70
71     message = xcalloc (1, sizeof (notmuch_message_t));
72
73     message->fd = open (filename, O_RDONLY);
74     if (message->fd < 0)
75         goto FAIL;
76
77     if (fstat (message->fd, &st) < 0)
78         goto FAIL;
79
80     message->map = mmap (NULL, st.st_size, PROT_READ, MAP_PRIVATE,
81                         message->fd, 0);
82     if (message->map == MAP_FAILED)
83         goto FAIL;
84
85     message->headers = g_hash_table_new_full (strcase_hash,
86                                               strcase_equal,
87                                               free,
88                                               free);
89
90     message->start = (char *) message->map;
91     message->size = st.st_size;
92     message->next_line = message->start;
93     message->parsing_started = 0;
94     message->parsing_finished = 0;
95
96     return message;
97
98   FAIL:
99     fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
100     notmuch_message_close (message);
101
102     return NULL;
103 }
104
105 void
106 notmuch_message_close (notmuch_message_t *message)
107 {
108     if (message == NULL)
109         return;
110
111     if (message->headers)
112         g_hash_table_unref (message->headers);
113
114     if (message->map)
115         munmap (message->map, message->size);
116     if (message->fd)
117         close (message->fd);
118
119     free (message);
120 }
121
122 void
123 notmuch_message_restrict_headersv (notmuch_message_t *message,
124                                    va_list va_headers)
125 {
126     char *header;
127
128     if (message->parsing_started ) {
129         fprintf (stderr, "Error: notmuch_message_restrict_headers called after parsing has started\n");
130         exit (1);
131     }
132
133     while (1) {
134         header = va_arg (va_headers, char*);
135         if (header == NULL)
136             break;
137         g_hash_table_insert (message->headers,
138                              xstrdup (header), NULL);
139     }
140
141     message->restrict_headers = 1;
142 }
143
144 void
145 notmuch_message_restrict_headers (notmuch_message_t *message, ...)
146 {
147     va_list va_headers;
148
149     va_start (va_headers, message);
150
151     notmuch_message_restrict_headersv (message, va_headers);
152 }
153
154 /* With our mmapped file, we don't get the benefit of terminated
155  * strings, so we can't use things like strchr(). We don't even know
156  * if there's a newline at the end of the file so we also have to be
157  * careful of that. Basically, every time we advance a pointer while
158  * parsing we must ensure we don't go beyond our buffer.
159  */
160 #define WITHIN(s) (((s) - message->start) < (message->size -1))
161
162 /* In each of the macros below, "without overrunning the buffer" means
163  * that the macro will never dereference a character beyond the end of
164  * the buffer. However, all of the macros may return a pointer
165  * pointing to the first character beyond the buffer. So callers
166  * should test with WITHIN before dereferencing the result. */
167
168 /* Advance 'ptr' until pointing at a non-space character in the same
169  * line, (without overrunning the buffer) */
170 #define SKIP_SPACE_IN_LINE(ptr)                               \
171     while (WITHIN (ptr) && (*(ptr) == ' ' || *(ptr) == '\t')) \
172         (ptr)++;
173
174 /* Advance 'ptr' until pointing at a non-space character, (without
175  * overrunning the buffer) */
176 #define SKIP_SPACE(ptr)                         \
177     while (WITHIN (ptr) && isspace(*(ptr)))     \
178         (ptr)++;
179
180 /* Advance 'ptr' to the first occurrence of 'c' within the same
181  * line, (without overrunning the buffer). */
182 #define ADVANCE_TO(ptr, c)                      \
183     while (WITHIN (ptr) && *(ptr) != '\n' &&    \
184            *(ptr) != (c))                       \
185     {                                           \
186         (ptr)++;                                \
187     }
188
189 /* Advance 'ptr' to the beginning of the next line not starting with
190  * an initial tab character, (without overruning the buffer). */
191 #define ADVANCE_TO_NEXT_HEADER_LINE(ptr)        \
192     do {                                        \
193         ADVANCE_TO ((ptr), '\n');               \
194         if (WITHIN (ptr))                       \
195             (ptr)++;                            \
196     } while (WITHIN (ptr) &&                    \
197              (*(ptr) == '\t' || *(ptr) == ' '));
198         
199 char *
200 copy_header_value (const char *start, const char *end)
201 {
202     const char *s;
203     char *result, *r;
204     int was_newline = 0;
205
206     result = xmalloc (end - start + 1);
207
208     s = start;
209     r = result;
210
211     while (s < end) {
212         if (*s == '\n') {
213             was_newline = 1;
214         } else {
215             if (*s == '\t' && was_newline)
216                 *r = ' ';
217             else
218                 *r = *s;
219             r++;
220             was_newline = 0;
221         }
222         s++;
223     }
224
225     *r = '\0';
226
227     return result;
228 }
229
230 const char *
231 notmuch_message_get_header (notmuch_message_t *message,
232                             const char *header_desired)
233 {
234     int contains;
235     const char *s, *colon;
236     char *header, *value;
237     int match;
238
239     message->parsing_started = 1;
240
241     contains = g_hash_table_lookup_extended (message->headers,
242                                              header_desired, NULL,
243                                              (gpointer *) &value);
244     if (contains)
245         return value;
246
247     if (message->parsing_finished)
248         return NULL;
249
250     while (1) {
251         s = message->next_line;
252
253         if (*s == '\n') {
254             message->parsing_finished = 1;
255             return NULL;
256         }
257
258         if (*s == '\t') {
259             fprintf (stderr, "Warning: Unexpected continued value\n");
260             ADVANCE_TO_NEXT_HEADER_LINE (message->next_line);
261             continue;
262         }
263
264         colon = s;
265         ADVANCE_TO (colon, ':');
266
267         if (! WITHIN (colon) || *colon == '\n') {
268             fprintf (stderr, "Warning: Unexpected non-header line: %s\n", s);
269             ADVANCE_TO_NEXT_HEADER_LINE (message->next_line);
270             continue;
271         }
272
273         header = xstrndup (s, colon - s);
274
275         if (message->restrict_headers &&
276             ! g_hash_table_lookup_extended (message->headers,
277                                             header, NULL, NULL))
278         {
279             free (header);
280             message->next_line = colon;
281             ADVANCE_TO_NEXT_HEADER_LINE (message->next_line);
282             continue;
283         }
284
285         s = colon + 1;
286         SKIP_SPACE_IN_LINE (s);
287
288         message->next_line = s;
289         ADVANCE_TO_NEXT_HEADER_LINE (message->next_line);
290
291         value = copy_header_value (s, message->next_line);
292
293         match = (strcasecmp (header, header_desired) == 0);
294
295         g_hash_table_insert (message->headers, header, value);
296
297         if (match)
298             return value;
299     }
300 }