tag-util.[ch]: New files for common tagging routines
[notmuch] / tag-util.c
1 #include <assert.h>
2 #include "string-util.h"
3 #include "tag-util.h"
4 #include "hex-escape.h"
5
6 #define TAG_OP_LIST_INITIAL_SIZE 10
7
8 struct _tag_operation_t {
9     const char *tag;
10     notmuch_bool_t remove;
11 };
12
13 struct _tag_op_list_t {
14     tag_operation_t *ops;
15     size_t count;
16     size_t size;
17 };
18
19 static tag_parse_status_t
20 line_error (tag_parse_status_t status,
21             const char *line,
22             const char *format, ...)
23 {
24     va_list va_args;
25
26     va_start (va_args, format);
27
28     fprintf (stderr, status < 0 ? "Error: " : "Warning: ");
29     vfprintf (stderr, format, va_args);
30     fprintf (stderr, " [%s]\n", line);
31     return status;
32 }
33
34 tag_parse_status_t
35 parse_tag_line (void *ctx, char *line,
36                 tag_op_flag_t flags,
37                 char **query_string,
38                 tag_op_list_t *tag_ops)
39 {
40     char *tok = line;
41     size_t tok_len = 0;
42     char *line_for_error;
43     int ret = 0;
44
45     chomp_newline (line);
46
47     line_for_error = talloc_strdup (ctx, line);
48     if (line_for_error == NULL) {
49         fprintf (stderr, "Error: out of memory\n");
50         return -1;
51     }
52
53     /* remove leading space */
54     while (*tok == ' ' || *tok == '\t')
55         tok++;
56
57     /* Skip empty and comment lines. */
58     if (*tok == '\0' || *tok == '#') {
59         ret = TAG_PARSE_SKIPPED;
60         goto DONE;
61     }
62
63     tag_op_list_reset (tag_ops);
64
65     /* Parse tags. */
66     while ((tok = strtok_len (tok + tok_len, " ", &tok_len)) != NULL) {
67         notmuch_bool_t remove;
68         char *tag;
69
70         /* Optional explicit end of tags marker. */
71         if (tok_len == 2 && strncmp (tok, "--", tok_len) == 0) {
72             tok = strtok_len (tok + tok_len, " ", &tok_len);
73             if (tok == NULL) {
74                 ret = line_error (TAG_PARSE_INVALID, line_for_error,
75                                   "no query string after --");
76                 goto DONE;
77             }
78             break;
79         }
80
81         /* Implicit end of tags. */
82         if (*tok != '-' && *tok != '+')
83             break;
84
85         /* If tag is terminated by NUL, there's no query string. */
86         if (*(tok + tok_len) == '\0') {
87             ret = line_error (TAG_PARSE_INVALID, line_for_error,
88                               "no query string");
89             goto DONE;
90         }
91
92         /* Terminate, and start next token after terminator. */
93         *(tok + tok_len++) = '\0';
94
95         remove = (*tok == '-');
96         tag = tok + 1;
97
98         /* Maybe refuse empty tags. */
99         if (! (flags & TAG_FLAG_BE_GENEROUS) && *tag == '\0') {
100             ret = line_error (TAG_PARSE_INVALID, line_for_error,
101                               "empty tag");
102             goto DONE;
103         }
104
105         /* Decode tag. */
106         if (hex_decode_inplace (tag) != HEX_SUCCESS) {
107             ret = line_error (TAG_PARSE_INVALID, line_for_error,
108                               "hex decoding of tag %s failed", tag);
109             goto DONE;
110         }
111
112         if (tag_op_list_append (ctx, tag_ops, tag, remove)) {
113             ret = line_error (TAG_PARSE_OUT_OF_MEMORY, line_for_error,
114                               "aborting");
115             goto DONE;
116         }
117     }
118
119     if (tok == NULL) {
120         /* use a different error message for testing */
121         ret = line_error (TAG_PARSE_INVALID, line_for_error,
122                           "missing query string");
123         goto DONE;
124     }
125
126     /* tok now points to the query string */
127     if (hex_decode_inplace (tok) != HEX_SUCCESS) {
128         ret = line_error (TAG_PARSE_INVALID, line_for_error,
129                           "hex decoding of query %s failed", tok);
130         goto DONE;
131     }
132
133     *query_string = tok;
134
135   DONE:
136     talloc_free (line_for_error);
137     return ret;
138 }
139
140 static inline void
141 message_error (notmuch_message_t *message,
142                notmuch_status_t status,
143                const char *format, ...)
144 {
145     va_list va_args;
146
147     va_start (va_args, format);
148
149     vfprintf (stderr, format, va_args);
150     fprintf (stderr, "Message-ID: %s\n", notmuch_message_get_message_id (message));
151     fprintf (stderr, "Status: %s\n", notmuch_status_to_string (status));
152 }
153
154 notmuch_status_t
155 tag_op_list_apply (notmuch_message_t *message,
156                    tag_op_list_t *list,
157                    tag_op_flag_t flags)
158 {
159     size_t i;
160     notmuch_status_t status = 0;
161     tag_operation_t *tag_ops = list->ops;
162
163     status = notmuch_message_freeze (message);
164     if (status) {
165         message_error (message, status, "freezing message");
166         return status;
167     }
168
169     if (flags & TAG_FLAG_REMOVE_ALL) {
170         status = notmuch_message_remove_all_tags (message);
171         if (status) {
172             message_error (message, status, "removing all tags");
173             return status;
174         }
175     }
176
177     for (i = 0; i < list->count; i++) {
178         if (tag_ops[i].remove) {
179             status = notmuch_message_remove_tag (message, tag_ops[i].tag);
180             if (status) {
181                 message_error (message, status, "removing tag %s", tag_ops[i].tag);
182                 return status;
183             }
184         } else {
185             status = notmuch_message_add_tag (message, tag_ops[i].tag);
186             if (status) {
187                 message_error (message, status, "adding tag %s", tag_ops[i].tag);
188                 return status;
189             }
190
191         }
192     }
193
194     status = notmuch_message_thaw (message);
195     if (status) {
196         message_error (message, status, "thawing message");
197         return status;
198     }
199
200
201     if (flags & TAG_FLAG_MAILDIR_SYNC) {
202         status = notmuch_message_tags_to_maildir_flags (message);
203         if (status) {
204             message_error (message, status, "synching tags to maildir");
205             return status;
206         }
207     }
208
209     return NOTMUCH_STATUS_SUCCESS;
210
211 }
212
213
214 /* Array of tagging operations (add or remove.  Size will be increased
215  * as necessary. */
216
217 tag_op_list_t *
218 tag_op_list_create (void *ctx)
219 {
220     tag_op_list_t *list;
221
222     list = talloc (ctx, tag_op_list_t);
223     if (list == NULL)
224         return NULL;
225
226     list->size = TAG_OP_LIST_INITIAL_SIZE;
227     list->count = 0;
228
229     list->ops = talloc_array (ctx, tag_operation_t, list->size);
230     if (list->ops == NULL)
231         return NULL;
232
233     return list;
234 }
235
236
237 int
238 tag_op_list_append (void *ctx,
239                     tag_op_list_t *list,
240                     const char *tag,
241                     notmuch_bool_t remove)
242 {
243     /* Make room if current array is full.  This should be a fairly
244      * rare case, considering the initial array size.
245      */
246
247     if (list->count == list->size) {
248         list->size *= 2;
249         list->ops = talloc_realloc (ctx, list->ops, tag_operation_t,
250                                     list->size);
251         if (list->ops == NULL) {
252             fprintf (stderr, "Out of memory.\n");
253             return 1;
254         }
255     }
256
257     /* add the new operation */
258
259     list->ops[list->count].tag = tag;
260     list->ops[list->count].remove = remove;
261     list->count++;
262     return 0;
263 }
264
265 /*
266  *   Is the i'th tag operation a remove?
267  */
268
269 notmuch_bool_t
270 tag_op_list_isremove (const tag_op_list_t *list, size_t i)
271 {
272     assert (i < list->count);
273     return list->ops[i].remove;
274 }
275
276 /*
277  * Reset a list to contain no operations
278  */
279
280 void
281 tag_op_list_reset (tag_op_list_t *list)
282 {
283     list->count = 0;
284 }
285
286 /*
287  * Return the number of operations in a list
288  */
289
290 size_t
291 tag_op_list_size (const tag_op_list_t *list)
292 {
293     return list->count;
294 }
295
296 /*
297  *   return the i'th tag in the list
298  */
299
300 const char *
301 tag_op_list_tag (const tag_op_list_t *list, size_t i)
302 {
303     assert (i < list->count);
304     return list->ops[i].tag;
305 }