tag-util: factor out rules for illegal tags, use in parse_tag_line
[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 /*
35  * Test tags for some forbidden cases.
36  *
37  * return: NULL if OK,
38  *         explanatory message otherwise.
39  */
40
41 static const char *
42 illegal_tag (const char *tag, notmuch_bool_t remove)
43 {
44
45     if (*tag == '\0' && ! remove)
46         return "empty tag forbidden";
47
48     /* This disallows adding the non-removable tag "-" and
49      * enables notmuch tag to take long options more easily.
50      */
51
52     if (*tag == '-' && ! remove)
53         return "tag starting with '-' forbidden";
54
55     return NULL;
56 }
57
58 tag_parse_status_t
59 parse_tag_line (void *ctx, char *line,
60                 tag_op_flag_t flags,
61                 char **query_string,
62                 tag_op_list_t *tag_ops)
63 {
64     char *tok = line;
65     size_t tok_len = 0;
66     char *line_for_error;
67     tag_parse_status_t ret = TAG_PARSE_SUCCESS;
68
69     chomp_newline (line);
70
71     line_for_error = talloc_strdup (ctx, line);
72     if (line_for_error == NULL) {
73         fprintf (stderr, "Error: out of memory\n");
74         return TAG_PARSE_OUT_OF_MEMORY;
75     }
76
77     /* remove leading space */
78     while (*tok == ' ' || *tok == '\t')
79         tok++;
80
81     /* Skip empty and comment lines. */
82     if (*tok == '\0' || *tok == '#') {
83         ret = TAG_PARSE_SKIPPED;
84         goto DONE;
85     }
86
87     tag_op_list_reset (tag_ops);
88
89     /* Parse tags. */
90     while ((tok = strtok_len (tok + tok_len, " ", &tok_len)) != NULL) {
91         notmuch_bool_t remove;
92         char *tag;
93
94         /* Optional explicit end of tags marker. */
95         if (tok_len == 2 && strncmp (tok, "--", tok_len) == 0) {
96             tok = strtok_len (tok + tok_len, " ", &tok_len);
97             if (tok == NULL) {
98                 ret = line_error (TAG_PARSE_INVALID, line_for_error,
99                                   "no query string after --");
100                 goto DONE;
101             }
102             break;
103         }
104
105         /* Implicit end of tags. */
106         if (*tok != '-' && *tok != '+')
107             break;
108
109         /* If tag is terminated by NUL, there's no query string. */
110         if (*(tok + tok_len) == '\0') {
111             ret = line_error (TAG_PARSE_INVALID, line_for_error,
112                               "no query string");
113             goto DONE;
114         }
115
116         /* Terminate, and start next token after terminator. */
117         *(tok + tok_len++) = '\0';
118
119         remove = (*tok == '-');
120         tag = tok + 1;
121
122         /* Maybe refuse illegal tags. */
123         if (! (flags & TAG_FLAG_BE_GENEROUS)) {
124             const char *msg = illegal_tag (tag, remove);
125             if (msg) {
126                 ret = line_error (TAG_PARSE_INVALID, line_for_error, msg);
127                 goto DONE;
128             }
129         }
130
131         /* Decode tag. */
132         if (hex_decode_inplace (tag) != HEX_SUCCESS) {
133             ret = line_error (TAG_PARSE_INVALID, line_for_error,
134                               "hex decoding of tag %s failed", tag);
135             goto DONE;
136         }
137
138         if (tag_op_list_append (tag_ops, tag, remove)) {
139             ret = line_error (TAG_PARSE_OUT_OF_MEMORY, line_for_error,
140                               "aborting");
141             goto DONE;
142         }
143     }
144
145     if (tok == NULL) {
146         /* use a different error message for testing */
147         ret = line_error (TAG_PARSE_INVALID, line_for_error,
148                           "missing query string");
149         goto DONE;
150     }
151
152     /* tok now points to the query string */
153     *query_string = tok;
154
155   DONE:
156     talloc_free (line_for_error);
157     return ret;
158 }
159
160 static inline void
161 message_error (notmuch_message_t *message,
162                notmuch_status_t status,
163                const char *format, ...)
164 {
165     va_list va_args;
166
167     va_start (va_args, format);
168
169     vfprintf (stderr, format, va_args);
170     fprintf (stderr, "Message-ID: %s\n", notmuch_message_get_message_id (message));
171     fprintf (stderr, "Status: %s\n", notmuch_status_to_string (status));
172 }
173
174 static int
175 makes_changes (notmuch_message_t *message,
176                tag_op_list_t *list,
177                tag_op_flag_t flags)
178 {
179
180     size_t i;
181
182     notmuch_tags_t *tags;
183     notmuch_bool_t changes = FALSE;
184
185     /* First, do we delete an existing tag? */
186     changes = FALSE;
187     for (tags = notmuch_message_get_tags (message);
188          ! changes && notmuch_tags_valid (tags);
189          notmuch_tags_move_to_next (tags)) {
190         const char *cur_tag = notmuch_tags_get (tags);
191         int last_op =  (flags & TAG_FLAG_REMOVE_ALL) ? -1 : 0;
192
193         /* scan backwards to get last operation */
194         i = list->count;
195         while (i > 0) {
196             i--;
197             if (strcmp (cur_tag, list->ops[i].tag) == 0) {
198                 last_op = list->ops[i].remove ? -1 : 1;
199                 break;
200             }
201         }
202
203         changes = (last_op == -1);
204     }
205     notmuch_tags_destroy (tags);
206
207     if (changes)
208         return TRUE;
209
210     /* Now check for adding new tags */
211     for (i = 0; i < list->count; i++) {
212         notmuch_bool_t exists = FALSE;
213
214         if (list->ops[i].remove)
215             continue;
216
217         for (tags = notmuch_message_get_tags (message);
218              notmuch_tags_valid (tags);
219              notmuch_tags_move_to_next (tags)) {
220             const char *cur_tag = notmuch_tags_get (tags);
221             if (strcmp (cur_tag, list->ops[i].tag) == 0) {
222                 exists = TRUE;
223                 break;
224             }
225         }
226         notmuch_tags_destroy (tags);
227
228         /* the following test is conservative,
229          * in the sense it ignores cases like +foo ... -foo
230          * but this is OK from a correctness point of view
231          */
232         if (! exists)
233             return TRUE;
234     }
235     return FALSE;
236
237 }
238
239 notmuch_status_t
240 tag_op_list_apply (notmuch_message_t *message,
241                    tag_op_list_t *list,
242                    tag_op_flag_t flags)
243 {
244     size_t i;
245     notmuch_status_t status = 0;
246     tag_operation_t *tag_ops = list->ops;
247
248     if (! (flags & TAG_FLAG_PRE_OPTIMIZED) && ! makes_changes (message, list, flags))
249         return NOTMUCH_STATUS_SUCCESS;
250
251     status = notmuch_message_freeze (message);
252     if (status) {
253         message_error (message, status, "freezing message");
254         return status;
255     }
256
257     if (flags & TAG_FLAG_REMOVE_ALL) {
258         status = notmuch_message_remove_all_tags (message);
259         if (status) {
260             message_error (message, status, "removing all tags");
261             return status;
262         }
263     }
264
265     for (i = 0; i < list->count; i++) {
266         if (tag_ops[i].remove) {
267             status = notmuch_message_remove_tag (message, tag_ops[i].tag);
268             if (status) {
269                 message_error (message, status, "removing tag %s", tag_ops[i].tag);
270                 return status;
271             }
272         } else {
273             status = notmuch_message_add_tag (message, tag_ops[i].tag);
274             if (status) {
275                 message_error (message, status, "adding tag %s", tag_ops[i].tag);
276                 return status;
277             }
278
279         }
280     }
281
282     status = notmuch_message_thaw (message);
283     if (status) {
284         message_error (message, status, "thawing message");
285         return status;
286     }
287
288
289     if (flags & TAG_FLAG_MAILDIR_SYNC) {
290         status = notmuch_message_tags_to_maildir_flags (message);
291         if (status) {
292             message_error (message, status, "synching tags to maildir");
293             return status;
294         }
295     }
296
297     return NOTMUCH_STATUS_SUCCESS;
298
299 }
300
301
302 /* Array of tagging operations (add or remove.  Size will be increased
303  * as necessary. */
304
305 tag_op_list_t *
306 tag_op_list_create (void *ctx)
307 {
308     tag_op_list_t *list;
309
310     list = talloc (ctx, tag_op_list_t);
311     if (list == NULL)
312         return NULL;
313
314     list->size = TAG_OP_LIST_INITIAL_SIZE;
315     list->count = 0;
316
317     list->ops = talloc_array (list, tag_operation_t, list->size);
318     if (list->ops == NULL)
319         return NULL;
320
321     return list;
322 }
323
324
325 int
326 tag_op_list_append (tag_op_list_t *list,
327                     const char *tag,
328                     notmuch_bool_t remove)
329 {
330     /* Make room if current array is full.  This should be a fairly
331      * rare case, considering the initial array size.
332      */
333
334     if (list->count == list->size) {
335         list->size *= 2;
336         list->ops = talloc_realloc (list, list->ops, tag_operation_t,
337                                     list->size);
338         if (list->ops == NULL) {
339             fprintf (stderr, "Out of memory.\n");
340             return 1;
341         }
342     }
343
344     /* add the new operation */
345
346     list->ops[list->count].tag = tag;
347     list->ops[list->count].remove = remove;
348     list->count++;
349     return 0;
350 }
351
352 /*
353  *   Is the i'th tag operation a remove?
354  */
355
356 notmuch_bool_t
357 tag_op_list_isremove (const tag_op_list_t *list, size_t i)
358 {
359     assert (i < list->count);
360     return list->ops[i].remove;
361 }
362
363 /*
364  * Reset a list to contain no operations
365  */
366
367 void
368 tag_op_list_reset (tag_op_list_t *list)
369 {
370     list->count = 0;
371 }
372
373 /*
374  * Return the number of operations in a list
375  */
376
377 size_t
378 tag_op_list_size (const tag_op_list_t *list)
379 {
380     return list->count;
381 }
382
383 /*
384  *   return the i'th tag in the list
385  */
386
387 const char *
388 tag_op_list_tag (const tag_op_list_t *list, size_t i)
389 {
390     assert (i < list->count);
391     return list->ops[i].tag;
392 }