1 /*
2   Copyright 2020 Northern.tech AS
3 
4   This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5 
6   This program is free software; you can redistribute it and/or modify it
7   under the terms of the GNU General Public License as published by the
8   Free Software Foundation; version 3.
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, write to the Free Software
17   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
18 
19   To the extent this program is licensed as part of the Enterprise
20   versions of CFEngine, the applicable Commercial Open Source License
21   (COSL) may apply to this file if you as a licensee so wish it. See
22   included file COSL.txt.
23 */
24 
25 #include <files_editline.h>
26 
27 #include <actuator.h>
28 #include <eval_context.h>
29 #include <promises.h>
30 #include <files_names.h>
31 #include <files_interfaces.h>
32 #include <vars.h>
33 #include <item_lib.h>
34 #include <sort.h>
35 #include <conversion.h>
36 #include <expand.h>
37 #include <scope.h>
38 #include <matching.h>
39 #include <match_scope.h>
40 #include <attributes.h>
41 #include <locks.h>
42 #include <string_lib.h>
43 #include <misc_lib.h>
44 #include <file_lib.h>
45 #include <rlist.h>
46 #include <policy.h>
47 #include <ornaments.h>
48 #include <verify_classes.h>
49 
50 #define CF_MAX_REPLACE 20
51 
52 /*****************************************************************************/
53 
54 enum editlinetypesequence
55 {
56     elp_vars,
57     elp_classes,
58     elp_delete,
59     elp_columns,
60     elp_insert,
61     elp_replace,
62     elp_reports,
63     elp_none
64 };
65 
66 static const char *const EDITLINETYPESEQUENCE[] =
67 {
68     "vars",
69     "classes",
70     "delete_lines",
71     "field_edits",
72     "insert_lines",
73     "replace_patterns",
74     "reports",
75     NULL
76 };
77 
78 static PromiseResult KeepEditLinePromise(EvalContext *ctx, const Promise *pp, void *param);
79 static PromiseResult VerifyLineDeletions(EvalContext *ctx, const Promise *pp, EditContext *edcontext);
80 static PromiseResult VerifyColumnEdits(EvalContext *ctx, const Promise *pp, EditContext *edcontext);
81 static PromiseResult VerifyPatterns(EvalContext *ctx, const Promise *pp, EditContext *edcontext);
82 static PromiseResult VerifyLineInsertions(EvalContext *ctx, const Promise *pp, EditContext *edcontext);
83 static bool InsertMultipleLinesToRegion(EvalContext *ctx, Item **start, Item *begin_ptr, Item *end_ptr, const Attributes *a, const Promise *pp, EditContext *edcontext, PromiseResult *result);
84 static bool InsertMultipleLinesAtLocation(EvalContext *ctx, Item **start, Item *begin_ptr, Item *end_ptr, Item *location, Item *prev, const Attributes *a, const Promise *pp, EditContext *edcontext, PromiseResult *result);
85 static bool DeletePromisedLinesMatching(EvalContext *ctx, Item **start, Item *begin, Item *end, const Attributes *a, const Promise *pp, EditContext *edcontext, PromiseResult *result);
86 static bool InsertLineAtLocation(EvalContext *ctx, char *newline, Item **start, Item *location, Item *prev, const Attributes *a, const Promise *pp, EditContext *edcontext, PromiseResult *result);
87 static bool InsertCompoundLineAtLocation(EvalContext *ctx, char *newline, Item **start, Item *begin_ptr, Item *end_ptr, Item *location, Item *prev, const Attributes *a, const Promise *pp, EditContext *edcontext, PromiseResult *result);
88 static int ReplacePatterns(EvalContext *ctx, Item *start, Item *end, const Attributes *a, const Promise *pp, EditContext *edcontext, PromiseResult *result);
89 static bool EditColumns(EvalContext *ctx, Item *file_start, Item *file_end, const Attributes *a, const Promise *pp, EditContext *edcontext, PromiseResult *result);
90 static bool EditLineByColumn(EvalContext *ctx, Rlist **columns, const Attributes *a, const Promise *pp, EditContext *edcontext, PromiseResult *result);
91 static bool DoEditColumn(Rlist **columns, EditContext *edcontext,
92                          EvalContext *ctx, const Promise *pp, const Attributes *a,
93                          PromiseResult *result);
94 static bool SanityCheckInsertions(const Attributes *a);
95 static bool SanityCheckDeletions(const Attributes *a, const Promise *pp);
96 static bool SelectLine(EvalContext *ctx, const char *line, const Attributes *a);
97 static bool NotAnchored(char *s);
98 static bool SelectRegion(EvalContext *ctx, Item *start, Item **begin_ptr, Item **end_ptr, const Attributes *a, EditContext *edcontext);
99 static bool MultiLineString(char *s);
100 static bool InsertFileAtLocation(EvalContext *ctx, Item **start, Item *begin_ptr, Item *end_ptr, Item *location, Item *prev, const Attributes *a, const Promise *pp, EditContext *edcontext, PromiseResult *result);
101 
102 /*****************************************************************************/
103 /* Level                                                                     */
104 /*****************************************************************************/
105 
ScheduleEditLineOperations(EvalContext * ctx,const Bundle * bp,const Attributes * a,const Promise * parentp,EditContext * edcontext)106 bool ScheduleEditLineOperations(EvalContext *ctx, const Bundle *bp, const Attributes *a, const Promise *parentp, EditContext *edcontext)
107 {
108     enum editlinetypesequence type;
109     char lockname[CF_BUFSIZE];
110     CfLock thislock;
111     int pass;
112 
113     assert(strcmp(bp->type, "edit_line") == 0);
114 
115     snprintf(lockname, CF_BUFSIZE - 1, "masterfilelock-%s", edcontext->filename);
116     thislock = AcquireLock(ctx, lockname, VUQNAME, CFSTARTTIME, a->transaction.ifelapsed, a->transaction.expireafter, parentp, true);
117 
118     if (thislock.lock == NULL)
119     {
120         return false;
121     }
122 
123     EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_EDIT, "filename", edcontext->filename, CF_DATA_TYPE_STRING, "source=promise");
124 
125     for (pass = 1; pass < CF_DONEPASSES; pass++)
126     {
127         for (type = 0; EDITLINETYPESEQUENCE[type] != NULL; type++)
128         {
129             const BundleSection *sp = BundleGetSection(bp, EDITLINETYPESEQUENCE[type]);
130             if (!sp)
131             {
132                 continue;
133             }
134 
135             EvalContextStackPushBundleSectionFrame(ctx, sp);
136             for (size_t ppi = 0; ppi < SeqLength(sp->promises); ppi++)
137             {
138                 Promise *pp = SeqAt(sp->promises, ppi);
139 
140                 ExpandPromise(ctx, pp, KeepEditLinePromise, edcontext);
141 
142                 if (BundleAbort(ctx))
143                 {
144                     YieldCurrentLock(thislock);
145                     EvalContextStackPopFrame(ctx);
146                     return false;
147                 }
148             }
149             EvalContextStackPopFrame(ctx);
150         }
151     }
152 
153     YieldCurrentLock(thislock);
154     return true;
155 }
156 
157 /*****************************************************************************/
158 
MakeTemporaryBundleFromTemplate(EvalContext * ctx,Policy * policy,const Attributes * a,const Promise * pp,PromiseResult * result)159 Bundle *MakeTemporaryBundleFromTemplate(EvalContext *ctx, Policy *policy, const Attributes *a, const Promise *pp, PromiseResult *result)
160 {
161     FILE *fp = NULL;
162     if ((fp = safe_fopen(a->edit_template, "rt" )) == NULL)
163     {
164         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, a, "Unable to open template file '%s' to make '%s'", a->edit_template, pp->promiser);
165         *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
166         return NULL;
167     }
168 
169     Bundle *bp = NULL;
170     {
171         char bundlename[CF_MAXVARSIZE];
172         snprintf(bundlename, CF_MAXVARSIZE, "temp_cf_bundle_%s", CanonifyName(a->edit_template));
173 
174         bp = PolicyAppendBundle(policy, "default", bundlename, "edit_line", NULL, NULL);
175     }
176     assert(bp);
177 
178     {
179         BundleSection *bsp = BundleAppendSection(bp, "insert_lines");
180         Promise *np = NULL;
181         Item *lines = NULL;
182         Item *stack = NULL;
183         char context[CF_BUFSIZE] = "any";
184         int lineno = 0;
185         size_t level = 0;
186 
187         size_t buffer_size = CF_BUFSIZE;
188         char *buffer = xmalloc(buffer_size);
189 
190         for (;;)
191         {
192             if (getline(&buffer, &buffer_size, fp) == -1)
193             {
194                 if (!feof(fp))
195                 {
196                     Log(LOG_LEVEL_ERR, "While constructing template for '%s', error reading. (getline %s)",
197                         pp->promiser, GetErrorStr());
198                     break;
199                 }
200                 else /* feof */
201                 {
202                     break;
203                 }
204             }
205 
206             lineno++;
207 
208             // Check closing syntax
209 
210             // Get Action operator
211             if (strncmp(buffer, "[%CFEngine", strlen("[%CFEngine")) == 0)
212             {
213                 char op[CF_BUFSIZE] = "";
214                 char brack[4]       = "";
215 
216                 sscanf(buffer+strlen("[%CFEngine"), "%1024s %3s", op, brack);
217 
218                 if (strcmp(brack, "%]") != 0)
219                 {
220                     cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, a, "Template file '%s' syntax error, missing close \"%%]\" at line %d", a->edit_template, lineno);
221                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
222                     return NULL;
223                 }
224 
225                 if (strcmp(op, "BEGIN") == 0)
226                 {
227                     PrependItem(&stack, context, NULL);
228                     if (++level > 1)
229                     {
230                         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, a, "Template file '%s' contains nested blocks which are not allowed, near line %d", a->edit_template, lineno);
231                         *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
232                         return NULL;
233                     }
234 
235                     continue;
236                 }
237 
238                 if (strcmp(op, "END") == 0)
239                 {
240                     level--;
241                     if (stack != NULL)
242                        {
243                        strcpy(context, stack->name);
244                        DeleteItem(&stack, stack);
245                        }
246                 }
247 
248                 if (strcmp(op + strlen(op)-2, "::") == 0)
249                 {
250                     *(op + strlen(op)-2) = '\0';
251                     strcpy(context, op);
252                     continue;
253                 }
254 
255                 size_t size = 0;
256                 for (const Item *ip = lines; ip != NULL; ip = ip->next)
257                 {
258                     size += strlen(ip->name);
259                 }
260 
261                 char *promiser = NULL;
262                 char *sp = promiser = xcalloc(1, size+1);
263 
264                 for (const Item *ip = lines; ip != NULL; ip = ip->next)
265                 {
266                     const int len = strlen(ip->name);
267                     memcpy(sp, ip->name, len);
268                     sp += len;
269                 }
270 
271                 int nl = StripTrailingNewline(promiser, size);
272                 CF_ASSERT(nl != -1, "StripTrailingNewline failure");
273 
274                 np = BundleSectionAppendPromise(bsp, promiser, (Rval) { NULL, RVAL_TYPE_NOPROMISEE }, context, NULL);
275                 np->offset.line = lineno;
276                 PromiseAppendConstraint(np, "insert_type", RvalNew("preserve_all_lines", RVAL_TYPE_SCALAR), false);
277 
278                 DeleteItemList(lines);
279                 free(promiser);
280                 lines = NULL;
281             }
282             else
283             {
284                 if (IsDefinedClass(ctx, context))
285                 {
286                     if (level > 0)
287                     {
288                         AppendItem(&lines, buffer, context);
289                     }
290                     else
291                     {
292                         //install independent promise line
293                         StripTrailingNewline(buffer, buffer_size);
294                         np = BundleSectionAppendPromise(bsp, buffer, (Rval) { NULL, RVAL_TYPE_NOPROMISEE }, context, NULL);
295                         np->offset.line = lineno;
296                         PromiseAppendConstraint(np, "insert_type", RvalNew("preserve_all_lines", RVAL_TYPE_SCALAR), false);
297                     }
298                 }
299             }
300         }
301 
302         free(buffer);
303     }
304 
305     fclose(fp);
306 
307     return bp;
308 }
309 
310 /***************************************************************************/
311 /* Level                                                                   */
312 /***************************************************************************/
313 
KeepEditLinePromise(EvalContext * ctx,const Promise * pp,void * param)314 static PromiseResult KeepEditLinePromise(EvalContext *ctx, const Promise *pp, void *param)
315 {
316     EditContext *edcontext = param;
317 
318     PromiseBanner(ctx, pp);
319 
320     if (strcmp("classes", pp->parent_section->promise_type) == 0)
321     {
322         return VerifyClassPromise(ctx, pp, NULL);
323     }
324     else if (strcmp("delete_lines", pp->parent_section->promise_type) == 0)
325     {
326         return VerifyLineDeletions(ctx, pp, edcontext);
327     }
328     else if (strcmp("field_edits", pp->parent_section->promise_type) == 0)
329     {
330         return VerifyColumnEdits(ctx, pp, edcontext);
331     }
332     else if (strcmp("insert_lines", pp->parent_section->promise_type) == 0)
333     {
334         return VerifyLineInsertions(ctx, pp, edcontext);
335     }
336     else if (strcmp("replace_patterns", pp->parent_section->promise_type) == 0)
337     {
338         return VerifyPatterns(ctx, pp, edcontext);
339     }
340     else if (strcmp("reports", pp->parent_section->promise_type) == 0)
341     {
342         return VerifyReportPromise(ctx, pp);
343     }
344 
345     return PROMISE_RESULT_NOOP;
346 }
347 
348 /***************************************************************************/
349 /* Level                                                                   */
350 /***************************************************************************/
351 
VerifyLineDeletions(EvalContext * ctx,const Promise * pp,EditContext * edcontext)352 static PromiseResult VerifyLineDeletions(EvalContext *ctx, const Promise *pp, EditContext *edcontext)
353 {
354     Item **start = &(edcontext->file_start);
355     Item *begin_ptr, *end_ptr;
356     CfLock thislock;
357     char lockname[CF_BUFSIZE];
358 
359     Attributes a = GetDeletionAttributes(ctx, pp);
360     a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
361 
362     if (!SanityCheckDeletions(&a, pp))
363     {
364         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, &a,
365              "The promised line deletion '%s' is inconsistent", pp->promiser);
366         return PROMISE_RESULT_INTERRUPTED;
367     }
368 
369 /* Are we working in a restricted region? */
370 
371     PromiseResult result = PROMISE_RESULT_NOOP;
372     if (!a.haveregion)
373     {
374         begin_ptr = NULL;
375         end_ptr = NULL;
376     }
377     else if (!SelectRegion(ctx, *start, &begin_ptr, &end_ptr, &a, edcontext))
378     {
379         if (a.region.include_end || a.region.include_start)
380         {
381             cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, &a,
382                  "The promised line deletion '%s' could not select an edit region in '%s'"
383                  " (this is a good thing, as policy suggests deleting the markers)",
384                  pp->promiser, edcontext->filename);
385         }
386         else
387         {
388             cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, &a,
389                  "The promised line deletion '%s' could not select an edit region in '%s'"
390                  " (but the delimiters were expected in the file)",
391                  pp->promiser, edcontext->filename);
392         }
393         result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
394         return result;
395     }
396     if (!end_ptr && a.region.select_end && !a.region.select_end_match_eof)
397     {
398         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, &a,
399              "The promised end pattern '%s' was not found when selecting region to delete in '%s'",
400              a.region.select_end, edcontext->filename);
401         result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
402         return result;
403     }
404 
405     snprintf(lockname, CF_BUFSIZE - 1, "deleteline-%s-%s", pp->promiser, edcontext->filename);
406     thislock = AcquireLock(ctx, lockname, VUQNAME, CFSTARTTIME, a.transaction.ifelapsed, a.transaction.expireafter, pp, true);
407 
408     if (thislock.lock == NULL)
409     {
410         return PROMISE_RESULT_SKIPPED;
411     }
412 
413     if (DeletePromisedLinesMatching(ctx, start, begin_ptr, end_ptr, &a, pp, edcontext, &result))
414     {
415         (edcontext->num_edits)++;
416     }
417     switch(result)
418     {
419     case PROMISE_RESULT_NOOP:
420         cfPS(ctx, LOG_LEVEL_VERBOSE, result, pp, &a,
421              "No changes done for the delete_lines promise '%s'", pp->promiser);
422         break;
423     case PROMISE_RESULT_CHANGE:
424         cfPS(ctx, LOG_LEVEL_INFO, result, pp, &a,
425              "delete_lines promise '%s' repaired", pp->promiser);
426         break;
427     case PROMISE_RESULT_WARN:
428         cfPS(ctx, LOG_LEVEL_WARNING, result, pp, &a,
429              "Warnings encountered when actuating delete_lines promise '%s'", pp->promiser);
430         break;
431     default:
432         cfPS(ctx, LOG_LEVEL_ERR, result, pp, &a,
433              "Errors encountered when actuating delete_lines promise '%s'", pp->promiser);
434         break;
435     }
436 
437     YieldCurrentLock(thislock);
438 
439     return result;
440 }
441 
442 /***************************************************************************/
443 
VerifyColumnEdits(EvalContext * ctx,const Promise * pp,EditContext * edcontext)444 static PromiseResult VerifyColumnEdits(EvalContext *ctx, const Promise *pp, EditContext *edcontext)
445 {
446     Item **start = &(edcontext->file_start);
447     Item *begin_ptr, *end_ptr;
448     CfLock thislock;
449     char lockname[CF_BUFSIZE];
450 
451     Attributes a = GetColumnAttributes(ctx, pp);
452     a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
453 
454     if (a.column.column_separator == NULL)
455     {
456         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, &a,
457              "No field_separator in promise to edit by column for '%s'", pp->promiser);
458         PromiseRef(LOG_LEVEL_ERR, pp);
459         return PROMISE_RESULT_FAIL;
460     }
461 
462     if (a.column.select_column <= 0)
463     {
464         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, &a,
465              "No select_field in promise to edit '%s'", pp->promiser);
466         PromiseRef(LOG_LEVEL_ERR, pp);
467         return PROMISE_RESULT_FAIL;
468     }
469 
470     if (!a.column.column_value)
471     {
472         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, &a,
473              "No field_value is promised to column_edit '%s'", pp->promiser);
474         PromiseRef(LOG_LEVEL_ERR, pp);
475         return PROMISE_RESULT_FAIL;
476     }
477 
478 /* Are we working in a restricted region? */
479 
480     PromiseResult result = PROMISE_RESULT_NOOP;
481     if (!a.haveregion)
482     {
483         begin_ptr = *start;
484         end_ptr = NULL;         // EndOfList(*start);
485     }
486     else if (!SelectRegion(ctx, *start, &begin_ptr, &end_ptr, &a, edcontext))
487     {
488         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, &a,
489              "The promised column edit '%s' could not select an edit region in '%s'",
490              pp->promiser, edcontext->filename);
491         result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
492         return result;
493     }
494 
495 /* locate and split line */
496 
497     snprintf(lockname, CF_BUFSIZE - 1, "column-%s-%s", pp->promiser, edcontext->filename);
498     thislock = AcquireLock(ctx, lockname, VUQNAME, CFSTARTTIME, a.transaction.ifelapsed, a.transaction.expireafter, pp, true);
499     if (thislock.lock == NULL)
500     {
501         return PROMISE_RESULT_SKIPPED;
502     }
503 
504     if (EditColumns(ctx, begin_ptr, end_ptr, &a, pp, edcontext, &result))
505     {
506         (edcontext->num_edits)++;
507     }
508     switch(result)
509     {
510     case PROMISE_RESULT_NOOP:
511         cfPS(ctx, LOG_LEVEL_VERBOSE, result, pp, &a,
512              "No changes done for the fields_edit promise '%s'", pp->promiser);
513         break;
514     case PROMISE_RESULT_CHANGE:
515         cfPS(ctx, LOG_LEVEL_INFO, result, pp, &a,
516              "fields_edit promise '%s' repaired", pp->promiser);
517         break;
518     case PROMISE_RESULT_WARN:
519         cfPS(ctx, LOG_LEVEL_WARNING, result, pp, &a,
520              "Warnings encountered when actuating fields_edit promise '%s'", pp->promiser);
521         break;
522     default:
523         cfPS(ctx, LOG_LEVEL_ERR, result, pp, &a,
524              "Errors encountered when actuating fields_edit promise '%s'", pp->promiser);
525         break;
526     }
527 
528     YieldCurrentLock(thislock);
529 
530     return result;
531 }
532 
533 /***************************************************************************/
534 
VerifyPatterns(EvalContext * ctx,const Promise * pp,EditContext * edcontext)535 static PromiseResult VerifyPatterns(EvalContext *ctx, const Promise *pp, EditContext *edcontext)
536 {
537     Item **start = &(edcontext->file_start);
538     Item *begin_ptr, *end_ptr;
539     CfLock thislock;
540     char lockname[CF_BUFSIZE];
541 
542     Log(LOG_LEVEL_VERBOSE, "Looking at pattern '%s'", pp->promiser);
543 
544 /* Are we working in a restricted region? */
545 
546     Attributes a = GetReplaceAttributes(ctx, pp);
547     a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
548 
549     if (!a.replace.replace_value)
550     {
551         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, &a,
552              "The promised pattern replace '%s' has no replacement string", pp->promiser);
553         return PROMISE_RESULT_FAIL;
554     }
555 
556     PromiseResult result = PROMISE_RESULT_NOOP;
557     if (!a.haveregion)
558     {
559         begin_ptr = *start;
560         end_ptr = NULL;         //EndOfList(*start);
561     }
562     else if (!SelectRegion(ctx, *start, &begin_ptr, &end_ptr, &a, edcontext))
563     {
564         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, &a,
565              "The promised pattern replace '%s' could not select an edit region in '%s'",
566              pp->promiser, edcontext->filename);
567         result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
568         return result;
569     }
570 
571     snprintf(lockname, CF_BUFSIZE - 1, "replace-%s-%s", pp->promiser, edcontext->filename);
572     thislock = AcquireLock(ctx, lockname, VUQNAME, CFSTARTTIME, a.transaction.ifelapsed, a.transaction.expireafter, pp, true);
573 
574     if (thislock.lock == NULL)
575     {
576         return PROMISE_RESULT_SKIPPED;
577     }
578 
579 /* Make sure back references are expanded */
580 
581     if (ReplacePatterns(ctx, begin_ptr, end_ptr, &a, pp, edcontext, &result))
582     {
583         (edcontext->num_edits)++;
584     }
585 
586     EvalContextVariableClearMatch(ctx);
587 
588     switch(result)
589     {
590     case PROMISE_RESULT_NOOP:
591         cfPS(ctx, LOG_LEVEL_VERBOSE, result, pp, &a,
592              "No changes done for the replace_patterns promise '%s'", pp->promiser);
593         break;
594     case PROMISE_RESULT_CHANGE:
595         cfPS(ctx, LOG_LEVEL_INFO, result, pp, &a,
596              "replace_patterns promise '%s' repaired", pp->promiser);
597         break;
598     case PROMISE_RESULT_WARN:
599         cfPS(ctx, LOG_LEVEL_WARNING, result, pp, &a,
600              "Warnings encountered when actuating replace_patterns promise '%s'", pp->promiser);
601         break;
602     default:
603         cfPS(ctx, LOG_LEVEL_ERR, result, pp, &a,
604              "Errors encountered when actuating replace_patterns promise '%s'", pp->promiser);
605         break;
606     }
607 
608     YieldCurrentLock(thislock);
609 
610     return result;
611 }
612 
613 /***************************************************************************/
614 
SelectNextItemMatching(EvalContext * ctx,const char * regexp,Item * begin,Item * end,Item ** match,Item ** prev)615 static bool SelectNextItemMatching(EvalContext *ctx, const char *regexp, Item *begin, Item *end, Item **match, Item **prev)
616 {
617     Item *ip_prev = NULL;
618 
619     *match = NULL;
620     *prev = NULL;
621 
622     for (Item *ip = begin; ip != end; ip = ip->next)
623     {
624         if (ip->name == NULL)
625         {
626             continue;
627         }
628 
629         if (FullTextMatch(ctx, regexp, ip->name))
630         {
631             *match = ip;
632             *prev = ip_prev;
633             return true;
634         }
635 
636         ip_prev = ip;
637     }
638 
639     return false;
640 }
641 
642 /***************************************************************************/
643 
SelectLastItemMatching(EvalContext * ctx,const char * regexp,Item * begin,Item * end,Item ** match,Item ** prev)644 static bool SelectLastItemMatching(EvalContext *ctx, const char *regexp, Item *begin, Item *end, Item **match, Item **prev)
645 {
646     Item *ip, *ip_last = NULL, *ip_prev = NULL;
647 
648     *match = NULL;
649     *prev = NULL;
650 
651     for (ip = begin; ip != end; ip = ip->next)
652     {
653         if (ip->name == NULL)
654         {
655             continue;
656         }
657 
658         if (FullTextMatch(ctx, regexp, ip->name))
659         {
660             *prev = ip_prev;
661             ip_last = ip;
662         }
663 
664         ip_prev = ip;
665     }
666 
667     if (ip_last)
668     {
669         *match = ip_last;
670         return true;
671     }
672 
673     return false;
674 }
675 
676 /***************************************************************************/
677 
SelectItemMatching(EvalContext * ctx,Item * start,char * regex,Item * begin_ptr,Item * end_ptr,Item ** match,Item ** prev,char * fl)678 static bool SelectItemMatching(EvalContext *ctx, Item *start, char *regex, Item *begin_ptr, Item *end_ptr, Item **match, Item **prev, char *fl)
679 {
680     Item *ip;
681     bool ret = false;
682 
683     *match = NULL;
684     *prev = NULL;
685 
686     if (regex == NULL)
687     {
688         return false;
689     }
690 
691     if (fl && (strcmp(fl, "first") == 0))
692     {
693         if (SelectNextItemMatching(ctx, regex, begin_ptr, end_ptr, match, prev))
694         {
695             ret = true;
696         }
697     }
698     else
699     {
700         if (SelectLastItemMatching(ctx, regex, begin_ptr, end_ptr, match, prev))
701         {
702             ret = true;
703         }
704     }
705 
706     if ((*match != NULL) && (*prev == NULL))
707     {
708         for (ip = start; (ip != NULL) && (ip != *match); ip = ip->next)
709         {
710             *prev = ip;
711         }
712     }
713 
714     return ret;
715 }
716 
717 /***************************************************************************/
718 
VerifyLineInsertions(EvalContext * ctx,const Promise * pp,EditContext * edcontext)719 static PromiseResult VerifyLineInsertions(EvalContext *ctx, const Promise *pp, EditContext *edcontext)
720 {
721     Item **start = &(edcontext->file_start), *match, *prev;
722     Item *begin_ptr, *end_ptr;
723     CfLock thislock;
724     char lockname[CF_BUFSIZE];
725 
726     Attributes a = GetInsertionAttributes(ctx, pp);
727     int allow_multi_lines = a.sourcetype && strcmp(a.sourcetype, "preserve_all_lines") == 0;
728     a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
729 
730     if (!SanityCheckInsertions(&a))
731     {
732         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, &a,
733              "The promised line insertion '%s' breaks its own promises", pp->promiser);
734         return PROMISE_RESULT_FAIL;
735     }
736 
737     /* Are we working in a restricted region? */
738 
739     PromiseResult result = PROMISE_RESULT_NOOP;
740 
741     if (!a.haveregion)
742     {
743         begin_ptr = *start;
744         end_ptr = NULL;         //EndOfList(*start);
745     }
746     else if (!SelectRegion(ctx, *start, &begin_ptr, &end_ptr, &a, edcontext))
747     {
748         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, &a,
749              "The promised line insertion '%s' could not select an edit region in '%s'",
750              pp->promiser, edcontext->filename);
751         result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
752         return result;
753     }
754 
755     if (!end_ptr && a.region.select_end && !a.region.select_end_match_eof)
756     {
757         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, &a,
758              "The promised end pattern '%s' was not found when selecting region to insert in '%s'",
759              a.region.select_end, edcontext->filename);
760         result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
761         return result;
762     }
763 
764     if (allow_multi_lines)
765     {
766         // promise to insert duplicates on first pass only
767         snprintf(lockname, CF_BUFSIZE - 1, "insertline-%s-%s-%lu", pp->promiser, edcontext->filename, (long unsigned int) pp->offset.line);
768     }
769     else
770     {
771         snprintf(lockname, CF_BUFSIZE - 1, "insertline-%s-%s", pp->promiser, edcontext->filename);
772     }
773 
774     thislock = AcquireLock(ctx, lockname, VUQNAME, CFSTARTTIME, a.transaction.ifelapsed, a.transaction.expireafter, pp, true);
775     if (thislock.lock == NULL)
776     {
777         return PROMISE_RESULT_SKIPPED;
778     }
779 
780     /* Are we looking for an anchored line inside the region? */
781 
782     if (a.location.line_matching == NULL)
783     {
784         if (InsertMultipleLinesToRegion(ctx, start, begin_ptr, end_ptr, &a, pp, edcontext, &result))
785         {
786             (edcontext->num_edits)++;
787         }
788     }
789     else
790     {
791         if (!SelectItemMatching(ctx, *start, a.location.line_matching, begin_ptr, end_ptr, &match, &prev, a.location.first_last))
792         {
793             cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, &a,
794                  "The promised line insertion '%s' could not select a locator matching regex '%s' in '%s'",
795                  pp->promiser, a.location.line_matching, edcontext->filename);
796             result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
797             YieldCurrentLock(thislock);
798             return result;
799         }
800 
801         if (InsertMultipleLinesAtLocation(ctx, start, begin_ptr, end_ptr, match, prev, &a, pp, edcontext, &result))
802         {
803             (edcontext->num_edits)++;
804         }
805     }
806 
807     switch(result)
808     {
809     case PROMISE_RESULT_NOOP:
810         cfPS(ctx, LOG_LEVEL_VERBOSE, result, pp, &a,
811              "No changes done for the insert_lines promise '%s'", pp->promiser);
812         break;
813     case PROMISE_RESULT_CHANGE:
814         cfPS(ctx, LOG_LEVEL_INFO, result, pp, &a,
815              "insert_lines promise '%s' repaired", pp->promiser);
816         break;
817     case PROMISE_RESULT_WARN:
818         cfPS(ctx, LOG_LEVEL_WARNING, result, pp, &a,
819              "Warnings encountered when actuating insert_lines promise '%s'", pp->promiser);
820         break;
821     default:
822         cfPS(ctx, LOG_LEVEL_ERR, result, pp, &a,
823              "Errors encountered when actuating insert_lines promise '%s'", pp->promiser);
824         break;
825     }
826 
827     YieldCurrentLock(thislock);
828 
829     return result;
830 }
831 
832 /***************************************************************************/
833 /* Level                                                                   */
834 /***************************************************************************/
835 
SelectRegion(EvalContext * ctx,Item * start,Item ** begin_ptr,Item ** end_ptr,const Attributes * a,EditContext * edcontext)836 static bool SelectRegion(EvalContext *ctx, Item *start,
837                          Item **begin_ptr, Item **end_ptr,
838                          const Attributes *a, EditContext *edcontext)
839 /*
840 
841 This should provide pointers to the first and last line of text that include the
842 delimiters, since we need to include those in case they are being deleted, etc.
843 It returns true if a match was identified, else false.
844 
845 If no such region matches, begin_ptr and end_ptr should point to NULL
846 
847 */
848 {
849     const char *const select_start = a->region.select_start;
850     const char *const select_end = a->region.select_end;
851     const int include_start = a->region.include_start;
852 
853     Item *ip, *beg = NULL, *end = NULL;
854 
855     for (ip = start; ip != NULL; ip = ip->next)
856     {
857         if (select_start)
858         {
859             if (!beg && FullTextMatch(ctx, select_start, ip->name))
860             {
861                 if (!include_start)
862                 {
863                     if (ip->next == NULL)
864                     {
865                         Log(LOG_LEVEL_VERBOSE,
866                              "The promised start pattern '%s' found an empty region at the end of file '%s'",
867                              select_start, edcontext->filename);
868                         return false;
869                     }
870                 }
871 
872                 beg = ip;
873                 continue;
874             }
875         }
876 
877         if (select_end && beg)
878         {
879             if (!end && FullTextMatch(ctx, select_end, ip->name))
880             {
881                 end = ip;
882                 break;
883             }
884         }
885 
886         if (beg && end)
887         {
888             break;
889         }
890     }
891 
892     if (!beg && select_start)
893     {
894         Log(LOG_LEVEL_VERBOSE,
895              "The promised start pattern '%s' was not found when selecting edit region in '%s'",
896              select_start, edcontext->filename);
897         return false;
898     }
899 
900     *begin_ptr = beg;
901     *end_ptr = end;
902 
903     return true;
904 }
905 
906 /*****************************************************************************/
907 
MatchRegion(EvalContext * ctx,const char * chunk,const Item * begin,const Item * end,bool regex)908 static int MatchRegion(EvalContext *ctx, const char *chunk, const Item *begin, const Item *end, bool regex)
909 /*
910   Match a region in between the selection delimiters. It is
911   called after SelectRegion. The end delimiter will be visible
912   here so we have to check for it. Can handle multi-line chunks
913 */
914 {
915     const Item *ip = begin;
916     size_t buf_size = strlen(chunk) + 1;
917     char *buf = xmalloc(buf_size);
918     int lines = 0;
919 
920     for (const char *sp = chunk; sp <= chunk + strlen(chunk); sp++)
921     {
922         buf[0] = '\0';
923         sscanf(sp, "%[^\n]", buf);
924         sp += strlen(buf);
925 
926         if (ip == NULL)
927         {
928             lines = 0;
929             goto bad;
930         }
931 
932         if (!regex && strcmp(buf, ip->name) != 0)
933         {
934             lines = 0;
935             goto bad;
936         }
937         if (regex && !FullTextMatch(ctx, buf, ip->name))
938         {
939             lines = 0;
940             goto bad;
941         }
942 
943         lines++;
944 
945         // We have to manually exclude the marked terminator
946 
947         if (ip == end)
948         {
949             lines = 0;
950             goto bad;
951         }
952 
953         // Now see if there is more
954 
955         if (ip->next)
956         {
957             ip = ip->next;
958         }
959         else                    // if the region runs out before the end
960         {
961             if (++sp <= chunk + strlen(chunk))
962             {
963                 lines = 0;
964                 goto bad;
965             }
966 
967             break;
968         }
969     }
970 
971 bad:
972     free(buf);
973     return lines;
974 }
975 
976 /*****************************************************************************/
977 
InsertMultipleLinesToRegion(EvalContext * ctx,Item ** start,Item * begin_ptr,Item * end_ptr,const Attributes * a,const Promise * pp,EditContext * edcontext,PromiseResult * result)978 static bool InsertMultipleLinesToRegion(EvalContext *ctx, Item **start, Item *begin_ptr, Item *end_ptr, const Attributes *a,
979                                        const Promise *pp, EditContext *edcontext, PromiseResult *result)
980 {
981     Item *ip, *prev = NULL;
982     int allow_multi_lines = StringEqual(a->sourcetype, "preserve_all_lines");
983 
984     // Insert at the start of the file
985 
986     if (*start == NULL)
987     {
988         return InsertMultipleLinesAtLocation(ctx, start, begin_ptr, end_ptr, *start, prev, a, pp, edcontext, result);
989     }
990 
991     // Insert at the start of the region
992 
993     if (a->location.before_after == EDIT_ORDER_BEFORE)
994     {
995         /* As region was already selected by SelectRegion() and we know
996          * what are the region boundaries (begin_ptr and end_ptr) there
997          * is no reason to iterate over whole file. */
998         for (ip = begin_ptr; ip != NULL; ip = ip->next)
999         {
1000             if (ip == begin_ptr)
1001             {
1002                 return InsertMultipleLinesAtLocation(ctx, start, begin_ptr, end_ptr, ip, prev, a, pp, edcontext, result);
1003             }
1004 
1005             prev = ip;
1006         }
1007     }
1008 
1009     // Insert at the end of the region / else end of the file
1010 
1011     if (a->location.before_after == EDIT_ORDER_AFTER)
1012     {
1013         /* As region was already selected by SelectRegion() and we know
1014          * what are the region boundaries (begin_ptr and end_ptr) there
1015          * is no reason to iterate over whole file. It is safe to start from
1016          * begin_ptr.
1017          * As a bonus Redmine #7640 is fixed as we are not interested in
1018          * matching values outside of the region we are iterating over. */
1019         for (ip = begin_ptr; ip != NULL; ip = ip->next)
1020         {
1021             if (!allow_multi_lines && MatchRegion(ctx, pp->promiser, ip, end_ptr, false))
1022             {
1023                 RecordNoChange(ctx, pp, a, "Promised chunk '%s' exists within selected region of %s",
1024                                pp->promiser, edcontext->filename);
1025                 return false;
1026             }
1027 
1028             if (ip->next != NULL && ip->next == end_ptr)
1029             {
1030                 return InsertMultipleLinesAtLocation(ctx, start, begin_ptr, end_ptr, ip, prev, a, pp, edcontext, result);
1031             }
1032 
1033             if (ip->next == NULL)
1034             {
1035                 return InsertMultipleLinesAtLocation(ctx, start, begin_ptr, end_ptr, ip, prev, a, pp, edcontext, result);
1036             }
1037 
1038             prev = ip;
1039         }
1040     }
1041 
1042     return false;
1043 }
1044 
1045 /***************************************************************************/
1046 
InsertMultipleLinesAtLocation(EvalContext * ctx,Item ** start,Item * begin_ptr,Item * end_ptr,Item * location,Item * prev,const Attributes * a,const Promise * pp,EditContext * edcontext,PromiseResult * result)1047 static bool InsertMultipleLinesAtLocation(EvalContext *ctx, Item **start, Item *begin_ptr, Item *end_ptr, Item *location,
1048                                          Item *prev, const Attributes *a, const Promise *pp, EditContext *edcontext, PromiseResult *result)
1049 
1050 // Promises to insert a possibly multi-line promiser at the specificed location convergently,
1051 // i.e. no insertion will be made if a neighbouring line matches
1052 
1053 {
1054     const char *const type = a->sourcetype;
1055     int isfileinsert = StringEqual(type, "file") || StringEqual(type, "file_preserve_block");
1056 
1057     if (isfileinsert)
1058     {
1059         return InsertFileAtLocation(ctx, start, begin_ptr, end_ptr, location, prev, a, pp, edcontext, result);
1060     }
1061     else
1062     {
1063         return InsertCompoundLineAtLocation(ctx, pp->promiser, start, begin_ptr, end_ptr, location,
1064                                             prev, a, pp, edcontext, result);
1065     }
1066 }
1067 
1068 /***************************************************************************/
1069 
DeletePromisedLinesMatching(EvalContext * ctx,Item ** start,Item * begin,Item * end,const Attributes * a,const Promise * pp,EditContext * edcontext,PromiseResult * result)1070 static bool DeletePromisedLinesMatching(EvalContext *ctx, Item **start, Item *begin, Item *end, const Attributes *a,
1071                                        const Promise *pp, EditContext *edcontext, PromiseResult *result)
1072 {
1073     Item *ip, *np = NULL, *lp, *initiator = begin, *terminator = NULL;
1074     int i, matches, noedits = true;
1075     bool retval = false;
1076 
1077     if (start == NULL)
1078     {
1079         return false;
1080     }
1081 
1082 // Get a pointer from before the region so we can patch the hole later
1083 
1084     if (begin == NULL)
1085     {
1086         initiator = *start;
1087     }
1088     else
1089     {
1090         if (a->region.include_start)
1091         {
1092             initiator = begin;
1093         }
1094         else
1095         {
1096             initiator = begin->next;
1097         }
1098     }
1099 
1100     if (end == NULL)
1101     {
1102         terminator = NULL;
1103     }
1104     else
1105     {
1106         if (a->region.include_end)
1107         {
1108             terminator = end->next;
1109         }
1110         else
1111         {
1112             terminator = end;
1113         }
1114     }
1115 
1116 // Now do the deletion
1117 
1118     for (ip = initiator; ip != terminator && ip != NULL; ip = np)
1119     {
1120         if (a->not_matching)
1121         {
1122             matches = !MatchRegion(ctx, pp->promiser, ip, terminator, true);
1123         }
1124         else
1125         {
1126             matches = MatchRegion(ctx, pp->promiser, ip, terminator, true);
1127         }
1128 
1129         if (matches)
1130         {
1131             Log(LOG_LEVEL_VERBOSE, "Multi-line region (%d lines) matched text in the file", matches);
1132         }
1133         else
1134         {
1135             Log(LOG_LEVEL_DEBUG, "Multi-line region didn't match text in the file");
1136         }
1137 
1138         if (!SelectLine(ctx, ip->name, a))       // Start search from location
1139         {
1140             np = ip->next;
1141             continue;
1142         }
1143 
1144         if (matches)
1145         {
1146             Log(LOG_LEVEL_VERBOSE, "Delete chunk of %d lines", matches);
1147 
1148             if (!MakingChanges(ctx, pp, a, result, "delete line '%s' from %s",
1149                                ip->name, edcontext->filename))
1150             {
1151                 np = ip->next;
1152                 noedits = false;
1153             }
1154             else
1155             {
1156                 for (i = 1; i <= matches; i++)
1157                 {
1158                     RecordChange(ctx, pp, a, "Deleted the promised line %d '%s' from %s",
1159                                  i, ip->name, edcontext->filename);
1160                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1161                     retval = true;
1162                     noedits = false;
1163 
1164                     if (ip->name != NULL)
1165                     {
1166                         free(ip->name);
1167                     }
1168 
1169                     np = ip->next;
1170                     free((char *) ip);
1171 
1172                     lp = ip;
1173 
1174                     if (ip == *start)
1175                     {
1176                         if (initiator == *start)
1177                         {
1178                             initiator = np;
1179                         }
1180                         *start = np;
1181                     }
1182                     else
1183                     {
1184                         if (ip == initiator)
1185                         {
1186                             initiator = *start;
1187                         }
1188 
1189                         for (lp = initiator; lp->next != ip; lp = lp->next)
1190                         {
1191                         }
1192 
1193                         lp->next = np;
1194                     }
1195 
1196                     (edcontext->num_edits)++;
1197 
1198                     ip = np;
1199                 }
1200             }
1201         }
1202         else
1203         {
1204             np = ip->next;
1205         }
1206     }
1207 
1208     if (noedits)
1209     {
1210         RecordNoChange(ctx, pp, a, "No need to delete lines from %s, ok", edcontext->filename);
1211     }
1212 
1213     return retval;
1214 }
1215 
1216 /********************************************************************/
1217 
ReplacePatterns(EvalContext * ctx,Item * file_start,Item * file_end,const Attributes * a,const Promise * pp,EditContext * edcontext,PromiseResult * result)1218 static int ReplacePatterns(EvalContext *ctx, Item *file_start, Item *file_end, const Attributes *a,
1219                            const Promise *pp, EditContext *edcontext, PromiseResult *result)
1220 {
1221     char line_buff[CF_EXPANDSIZE];
1222     char after[CF_BUFSIZE];
1223     int match_len, start_off, end_off, once_only = false, retval = false;
1224     Item *ip;
1225     int notfound = true, cutoff = 1, replaced = false;
1226 
1227     if (StringEqual(a->replace.occurrences, "first"))
1228     {
1229         Log(LOG_LEVEL_WARNING, "Setting replace-occurrences policy to 'first' is not convergent");
1230         once_only = true;
1231     }
1232 
1233     Buffer *replace = BufferNew();
1234     for (ip = file_start; ip != NULL && ip != file_end; ip = ip->next)
1235     {
1236         if (ip->name == NULL)
1237         {
1238             continue;
1239         }
1240 
1241         cutoff = 1;
1242         strlcpy(line_buff, ip->name, sizeof(line_buff));
1243         replaced = false;
1244         match_len = 0;
1245 
1246         while (BlockTextMatch(ctx, pp->promiser, line_buff, &start_off, &end_off))
1247         {
1248             if (match_len == strlen(line_buff))
1249             {
1250                 Log(LOG_LEVEL_VERBOSE, "Improper convergent expression matches defacto convergence, so accepting");
1251                 break;
1252             }
1253 
1254             if (cutoff++ > CF_MAX_REPLACE)
1255             {
1256                 Log(LOG_LEVEL_VERBOSE, "Too many replacements on this line");
1257                 break;
1258             }
1259 
1260             match_len = end_off - start_off;
1261             BufferClear(replace);
1262             ExpandScalar(ctx, PromiseGetBundle(pp)->ns, PromiseGetBundle(pp)->name, a->replace.replace_value, replace);
1263 
1264             Log(LOG_LEVEL_VERBOSE, "Verifying replacement of '%s' with '%s', cutoff %d", pp->promiser, BufferData(replace),
1265                   cutoff);
1266 
1267             // Save portion of line after substitution:
1268             strlcpy(after, line_buff + end_off, sizeof(after));
1269             // TODO: gripe if that truncated !
1270 
1271             // Substitute into line_buff:
1272             snprintf(line_buff + start_off, sizeof(line_buff) - start_off,
1273                      "%s%s", BufferData(replace), after);
1274             // TODO: gripe if that truncated or failed !
1275             notfound = false;
1276             replaced = true;
1277 
1278             if (once_only)
1279             {
1280                 Log(LOG_LEVEL_VERBOSE, "Replace first occurrence only (warning, this is not a convergent policy)");
1281                 break;
1282             }
1283         }
1284 
1285         if (NotAnchored(pp->promiser) && BlockTextMatch(ctx, pp->promiser, line_buff, &start_off, &end_off))
1286         {
1287             RecordInterruption(ctx, pp, a,
1288                                "Promised replacement '%s' on line '%s' for pattern '%s'"
1289                                " is not convergent while editing '%s'"
1290                                " (regular expression matches the replacement string)",
1291                                line_buff, ip->name, pp->promiser, edcontext->filename);
1292             *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
1293             PromiseRef(LOG_LEVEL_ERR, pp);
1294             break;
1295         }
1296 
1297         if (!MakingChanges(ctx, pp, a, result, "replace pattern '%s' in '%s'", pp->promiser,
1298                            edcontext->filename))
1299         {
1300             continue;
1301         }
1302         else if (replaced)
1303         {
1304             free(ip->name);
1305             ip->name = xstrdup(line_buff);
1306             RecordChange(ctx, pp, a, "Replaced pattern '%s' in '%s'", pp->promiser, edcontext->filename);
1307             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1308             (edcontext->num_edits)++;
1309             retval = true;
1310 
1311             Log(LOG_LEVEL_VERBOSE, "cutoff %d, '%s'", cutoff, ip->name);
1312             Log(LOG_LEVEL_VERBOSE, "cutoff %d, '%s'", cutoff, line_buff);
1313 
1314             if (once_only)
1315             {
1316                 Log(LOG_LEVEL_VERBOSE, "Replace first occurrence only (warning, this is not a convergent policy)");
1317                 break;
1318             }
1319 
1320             if (BlockTextMatch(ctx, pp->promiser, ip->name, &start_off, &end_off))
1321             {
1322                 RecordInterruption(ctx, pp, a,
1323                                    "Promised replacement '%s' for pattern '%s' is not properly convergent while editing '%s'"
1324                                    " (pattern still matches the end-state replacement string '%s')",
1325                                    ip->name, pp->promiser, edcontext->filename, line_buff);
1326                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
1327                 PromiseRef(LOG_LEVEL_INFO, pp);
1328             }
1329         }
1330     }
1331 
1332     BufferDestroy(replace);
1333 
1334     if (notfound)
1335     {
1336         RecordNoChange(ctx, pp, a, "No match for pattern '%s' in '%s'", pp->promiser, edcontext->filename);
1337     }
1338 
1339     return retval;
1340 }
1341 
1342 /********************************************************************/
1343 
EditColumns(EvalContext * ctx,Item * file_start,Item * file_end,const Attributes * a,const Promise * pp,EditContext * edcontext,PromiseResult * result)1344 static bool EditColumns(EvalContext *ctx, Item *file_start, Item *file_end, const Attributes *a,
1345                         const Promise *pp, EditContext *edcontext, PromiseResult *result)
1346 {
1347     char separator[CF_MAXVARSIZE];
1348     int s, e;
1349     bool retval = false;
1350     Item *ip;
1351     Rlist *columns = NULL;
1352 
1353     if (!ValidateRegEx(pp->promiser))
1354     {
1355         return false;
1356     }
1357 
1358     bool found_match = false;
1359     for (ip = file_start; ip != file_end; ip = ip->next)
1360     {
1361         if (ip->name == NULL)
1362         {
1363             continue;
1364         }
1365 
1366         if (!FullTextMatch(ctx, pp->promiser, ip->name))
1367         {
1368             continue;
1369         }
1370         else
1371         {
1372             found_match = true;
1373             Log(LOG_LEVEL_VERBOSE, "Matched line '%s'", ip->name);
1374         }
1375 
1376         if (!BlockTextMatch(ctx, a->column.column_separator, ip->name, &s, &e))
1377         {
1378             RecordInterruption(ctx, pp, a, "Field edit, no fields found by promised pattern '%s' in '%s'",
1379                                a->column.column_separator, edcontext->filename);
1380             *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
1381             return false;
1382         }
1383 
1384         if (e - s > CF_MAXVARSIZE / 2)
1385         {
1386             Log(LOG_LEVEL_ERR, "Line split criterion matches a huge part of the line, seems to be in error");
1387             return false;
1388         }
1389 
1390         strlcpy(separator, ip->name + s, e - s + 1);
1391 
1392         columns = RlistFromSplitRegex(ip->name, a->column.column_separator, CF_INFINITY, a->column.blanks_ok);
1393         retval = EditLineByColumn(ctx, &columns, a, pp, edcontext, result);
1394 
1395         if (retval)
1396         {
1397             free(ip->name);
1398             ip->name = Rlist2String(columns, separator);
1399         }
1400 
1401         RlistDestroy(columns);
1402     }
1403 
1404     if (!found_match)
1405     {
1406         RecordFailure(ctx, pp, a,
1407                       "No matched line to edit fields of for pattern '%s' in '%s'",
1408                       pp->promiser, edcontext->filename);
1409         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1410     }
1411 
1412     return retval;
1413 }
1414 
1415 /***************************************************************************/
1416 
SanityCheckInsertions(const Attributes * a)1417 static bool SanityCheckInsertions(const Attributes *a)
1418 {
1419     long not = 0;
1420     long with = 0;
1421     bool ok = true;
1422     Rlist *rp;
1423     InsertMatchType opt;
1424     int exact = false, ignore_something = false;
1425     const bool preserve_block = StringEqual(a->sourcetype, "preserve_block");
1426     const LineSelect line_select = a->line_select;
1427 
1428     if (line_select.startwith_from_list)
1429     {
1430         with++;
1431     }
1432 
1433     if (line_select.not_startwith_from_list)
1434     {
1435         not++;
1436     }
1437 
1438     if (line_select.match_from_list)
1439     {
1440         with++;
1441     }
1442 
1443     if (line_select.not_match_from_list)
1444     {
1445         not++;
1446     }
1447 
1448     if (line_select.contains_from_list)
1449     {
1450         with++;
1451     }
1452 
1453     if (line_select.not_contains_from_list)
1454     {
1455         not++;
1456     }
1457 
1458     if (not > 1)
1459     {
1460         Log(LOG_LEVEL_ERR,
1461               "Line insertion selection promise is meaningless - the alternatives are mutually exclusive (only one is allowed)");
1462         ok = false;
1463     }
1464 
1465     if (with && not)
1466     {
1467         Log(LOG_LEVEL_ERR,
1468               "Line insertion selection promise is meaningless - cannot mix positive and negative constraints");
1469         ok = false;
1470     }
1471 
1472     for (rp = a->insert_match; rp != NULL; rp = rp->next)
1473     {
1474         opt = InsertMatchTypeFromString(RlistScalarValue(rp));
1475 
1476         if (opt == INSERT_MATCH_TYPE_EXACT)
1477         {
1478             exact = true;
1479         }
1480         else
1481         {
1482             ignore_something = true;
1483             if (preserve_block)
1484             {
1485                 Log(LOG_LEVEL_ERR, "Line insertion should not use whitespace policy with preserve_block");
1486                 ok = false;
1487             }
1488         }
1489     }
1490 
1491     if (exact && ignore_something)
1492     {
1493         Log(LOG_LEVEL_ERR,
1494               "Line insertion selection promise is meaningless - cannot mix exact_match with other ignore whitespace options");
1495         ok = false;
1496     }
1497 
1498     return ok;
1499 }
1500 
1501 /***************************************************************************/
1502 
SanityCheckDeletions(const Attributes * a,const Promise * pp)1503 static bool SanityCheckDeletions(const Attributes *a, const Promise *pp)
1504 {
1505     if (MultiLineString(pp->promiser))
1506     {
1507         if (a->not_matching)
1508         {
1509             Log(LOG_LEVEL_ERR,
1510                 "Makes no sense to promise multi-line delete with not_matching. Cannot be satisfied for all lines as a block.");
1511             // FIXME: This function always returns true (!)
1512         }
1513     }
1514 
1515     return true;
1516 }
1517 
1518 /***************************************************************************/
1519 
1520 /* XXX */
MatchPolicy(EvalContext * ctx,const char * camel,const char * haystack,Rlist * insert_match,const Promise * pp)1521 static bool MatchPolicy(EvalContext *ctx, const char *camel, const char *haystack, Rlist *insert_match, const Promise *pp)
1522 {
1523     char *final = NULL;
1524     bool ok      = false;
1525     bool escaped = false;
1526     Item *list = SplitString(camel, '\n');
1527 
1528     //Split into separate lines first
1529     for (Item *ip = list; ip != NULL; ip = ip->next)
1530     {
1531         ok = false;
1532         bool direct_cmp = (strcmp(camel, haystack) == 0);
1533 
1534         final             = xstrdup(ip->name);
1535         size_t final_size = strlen(final) + 1;
1536 
1537         if (insert_match == NULL)
1538         {
1539             // No whitespace policy means exact_match
1540             ok = ok || direct_cmp;
1541             break;
1542         }
1543 
1544         for (Rlist *rp = insert_match; rp != NULL; rp = rp->next)
1545         {
1546             const InsertMatchType opt =
1547                 InsertMatchTypeFromString(RlistScalarValue(rp));
1548 
1549             /* Exact match can be done immediately */
1550 
1551             if (opt == INSERT_MATCH_TYPE_EXACT)
1552             {
1553                 if ((rp->next != NULL) || (rp != insert_match))
1554                 {
1555                     Log(LOG_LEVEL_ERR, "Multiple policies conflict with \"exact_match\", using exact match");
1556                     PromiseRef(LOG_LEVEL_ERR, pp);
1557                 }
1558 
1559                 ok = ok || direct_cmp;
1560                 break;
1561             }
1562 
1563             if (!escaped)
1564             {
1565                 // Need to escape the original string once here in case it contains regex chars when non-exact match
1566                 // Check size of escaped string, and realloc if necessary
1567                 size_t escape_regex_len = EscapeRegexCharsLen(ip->name);
1568                 if (escape_regex_len + 1 > final_size)
1569                 {
1570                     final = xrealloc(final, escape_regex_len + 1);
1571                     final_size = escape_regex_len + 1;
1572                 }
1573 
1574                 EscapeRegexChars(ip->name, final, final_size);
1575                 escaped = true;
1576             }
1577 
1578             if (opt == INSERT_MATCH_TYPE_IGNORE_EMBEDDED)
1579             {
1580                 // Strip initial and final first
1581                 char *firstchar, *lastchar;
1582                 for (firstchar = final; isspace((int)*firstchar); firstchar++);
1583                 for (lastchar = final + strlen(final) - 1; (lastchar > firstchar) && (isspace((int)*lastchar)); lastchar--);
1584 
1585                 // Since we're stripping space and replacing it with \s+, we need to account for that
1586                 // when allocating work
1587                 size_t work_size = final_size + 6;        /* allocated size */
1588                 char  *work      = xcalloc(1, work_size);
1589 
1590                 /* We start only with the terminating '\0'. */
1591                 size_t required_size = 1;
1592 
1593                 for (char *sp = final; *sp != '\0'; sp++)
1594                 {
1595                     char toadd[4];
1596 
1597                     if ((sp > firstchar) && (sp < lastchar))
1598                     {
1599                         if (isspace((int)*sp))
1600                         {
1601                             while (isspace((int)*(sp + 1)))
1602                             {
1603                                 sp++;
1604                             }
1605 
1606                             required_size += 3;
1607                             strcpy(toadd, "\\s+");
1608                         }
1609                         else
1610                         {
1611                             required_size++;
1612                             toadd[0] = *sp;
1613                             toadd[1] = '\0';
1614                         }
1615                     }
1616                     else
1617                     {
1618                         required_size++;
1619                         toadd[0] = *sp;
1620                         toadd[1] = '\0';
1621                     }
1622 
1623                     if (required_size > work_size)
1624                     {
1625                         // Increase by a small amount extra, so we don't
1626                         // reallocate every iteration
1627                         work_size = required_size + 12;
1628                         work = xrealloc(work, work_size);
1629                     }
1630 
1631                     if (strlcat(work, toadd, work_size) >= work_size)
1632                     {
1633                         UnexpectedError("Truncation concatenating '%s' to: %s",
1634                                         toadd, work);
1635                     }
1636                 }
1637 
1638                 // Realloc and retry on truncation
1639                 if (strlcpy(final, work, final_size) >= final_size)
1640                 {
1641                     final = xrealloc(final, work_size);
1642                     final_size = work_size;
1643                     strlcpy(final, work, final_size);
1644                 }
1645 
1646                 free(work);
1647             }
1648             else if (opt == INSERT_MATCH_TYPE_IGNORE_LEADING)
1649             {
1650                 if (strncmp(final, "\\s*", 3) != 0)
1651                 {
1652                     char *sp;
1653                     for (sp = final; isspace((int)*sp); sp++);
1654 
1655                     size_t work_size = final_size + 3;
1656                     char  *work      = xcalloc(1, work_size);
1657                     strcpy(work, sp);
1658 
1659                     if (snprintf(final, final_size, "\\s*%s", work) >= final_size - 1)
1660                     {
1661                         final = xrealloc(final, work_size);
1662                         final_size = work_size;
1663                         snprintf(final, final_size, "\\s*%s", work);
1664                     }
1665 
1666                     free(work);
1667                 }
1668             }
1669             else if (opt == INSERT_MATCH_TYPE_IGNORE_TRAILING)
1670             {
1671                 if (strncmp(final + strlen(final) - 4, "\\s*", 3) != 0)
1672                 {
1673                     size_t work_size = final_size + 3;
1674                     char  *work      = xcalloc(1, work_size);
1675                     strcpy(work, final);
1676 
1677                     char *sp;
1678                     for (sp = work + strlen(work) - 1; (sp > work) && (isspace((int)*sp)); sp--);
1679                     *++sp = '\0';
1680                     if (snprintf(final, final_size, "%s\\s*", work) >= final_size - 1)
1681                     {
1682                         final = xrealloc(final, work_size);
1683                         final_size = work_size;
1684                         snprintf(final, final_size, "%s\\s*", work);
1685                     }
1686 
1687                     free(work);
1688                 }
1689             }
1690 
1691             ok = ok || (FullTextMatch(ctx, final, haystack));
1692         }
1693 
1694         assert(final_size > strlen(final));
1695         free(final);
1696         final = NULL;
1697         if (!ok)                // All lines in region need to match to avoid insertions
1698         {
1699             break;
1700         }
1701     }
1702 
1703     free(final);
1704     DeleteItemList(list);
1705     return ok;
1706 }
1707 
IsItemInRegion(EvalContext * ctx,const char * item,const Item * begin_ptr,const Item * end_ptr,Rlist * insert_match,const Promise * pp)1708 static bool IsItemInRegion(EvalContext *ctx, const char *item, const Item *begin_ptr, const Item *end_ptr, Rlist *insert_match, const Promise *pp)
1709 {
1710     for (const Item *ip = begin_ptr; ((ip != end_ptr) && (ip != NULL)); ip = ip->next)
1711     {
1712         if (MatchPolicy(ctx, item, ip->name, insert_match, pp))
1713         {
1714             return true;
1715         }
1716     }
1717 
1718     return false;
1719 }
1720 
1721 /***************************************************************************/
1722 
InsertFileAtLocation(EvalContext * ctx,Item ** start,Item * begin_ptr,Item * end_ptr,Item * location,Item * prev,const Attributes * a,const Promise * pp,EditContext * edcontext,PromiseResult * result)1723 static bool InsertFileAtLocation(EvalContext *ctx, Item **start, Item *begin_ptr, Item *end_ptr, Item *location,
1724                                 Item *prev, const Attributes *a, const Promise *pp, EditContext *edcontext, PromiseResult *result)
1725 {
1726     FILE *fin;
1727     bool retval = false;
1728     Item *loc = NULL;
1729     const bool preserve_block = StringEqual(a->sourcetype, "file_preserve_block");
1730 
1731     struct stat sb;
1732     if ((stat(pp->promiser, &sb) == 0) && S_ISDIR(sb.st_mode))
1733     {
1734         RecordInterruption(ctx, pp, a, "Could not insert lines from a directory '%s'", pp->promiser);
1735         *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
1736         return false;
1737     }
1738 
1739     if ((fin = safe_fopen(pp->promiser, "rt")) == NULL)
1740     {
1741         RecordInterruption(ctx, pp, a, "Could not read file '%s'. (fopen: %s)",
1742                            pp->promiser, GetErrorStr());
1743         *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
1744         return false;
1745     }
1746 
1747     size_t buf_size = CF_BUFSIZE;
1748     char *buf = xmalloc(buf_size);
1749     loc = location;
1750     Buffer *exp = BufferNew();
1751 
1752     while (CfReadLine(&buf, &buf_size, fin) != -1)
1753     {
1754         BufferClear(exp);
1755         if (a->expandvars)
1756         {
1757             ExpandScalar(ctx, PromiseGetBundle(pp)->ns, PromiseGetBundle(pp)->name, buf, exp);
1758         }
1759         else
1760         {
1761             BufferAppend(exp, buf, strlen(buf));
1762         }
1763 
1764         if (!SelectLine(ctx, BufferData(exp), a))
1765         {
1766             free(buf);
1767             buf = NULL;
1768             continue;
1769         }
1770 
1771         if (!preserve_block && IsItemInRegion(ctx, BufferData(exp), begin_ptr, end_ptr, a->insert_match, pp))
1772         {
1773             RecordNoChange(ctx, pp, a, "Promised file line '%s' exists within file '%s'",
1774                            BufferData(exp), edcontext->filename);
1775             free(buf);
1776             buf = NULL;
1777             continue;
1778         }
1779 
1780         // Need to call CompoundLine here in case ExpandScalar has inserted \n into a string
1781 
1782         if (InsertCompoundLineAtLocation(ctx, BufferGet(exp), start, begin_ptr, end_ptr,
1783                                          loc, prev, a, pp, edcontext, result))
1784         {
1785             retval = true;
1786         }
1787 
1788         if (preserve_block && !prev)
1789         {
1790             // If we are inserting a preserved block before, need to flip the implied order after the first insertion
1791             // to get the order of the block right
1792             //a->location.before_after = cfe_after;
1793         }
1794 
1795         if (prev)
1796         {
1797             prev = prev->next;
1798         }
1799         else
1800         {
1801             prev = *start;
1802         }
1803 
1804         if (loc)
1805         {
1806             loc = loc->next;
1807         }
1808         else
1809         {
1810             location = *start;
1811         }
1812 
1813         free(buf);
1814         buf = NULL;
1815     }
1816 
1817     if (buf != NULL)
1818     {
1819         free(buf);
1820     }
1821 
1822     if (ferror(fin))
1823     {
1824         if (errno == EISDIR)
1825         {
1826             RecordInterruption(ctx, pp, a, "'%s' is a directory, cannot lines into it", pp->promiser);
1827             *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
1828         }
1829         else
1830         {
1831             UnexpectedError("Failed to read line from stream");
1832             RecordFailure(ctx, pp, a, "Failed to read line from stream");
1833             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1834         }
1835     }
1836 
1837     fclose(fin);
1838     BufferDestroy(exp);
1839     return retval;
1840 }
1841 
1842 /***************************************************************************/
1843 
InsertCompoundLineAtLocation(EvalContext * ctx,char * chunk,Item ** start,Item * begin_ptr,Item * end_ptr,Item * location,Item * prev,const Attributes * a,const Promise * pp,EditContext * edcontext,PromiseResult * result)1844 static bool InsertCompoundLineAtLocation(EvalContext *ctx, char *chunk, Item **start, Item *begin_ptr, Item *end_ptr,
1845                                         Item *location, Item *prev, const Attributes *a, const Promise *pp, EditContext *edcontext,
1846                                         PromiseResult *result)
1847 {
1848     bool retval = false;
1849     const char *const type = a->sourcetype;
1850     const bool preserve_all_lines = StringEqual(type, "preserve_all_lines");
1851     const bool preserve_block = type && (preserve_all_lines || strcmp(type, "preserve_block") == 0 || strcmp(type, "file_preserve_block") == 0);
1852 
1853     if (!preserve_all_lines && MatchRegion(ctx, chunk, location, NULL, false))
1854     {
1855         RecordNoChange(ctx, pp, a,
1856                        "Promised chunk '%s' exists within selected region of %s (promise kept)",
1857                        pp->promiser, edcontext->filename);
1858         return false;
1859     }
1860 
1861     // Iterate over any lines within the chunk
1862 
1863     char *buf = NULL;
1864     size_t buf_size = 0;
1865     for (char *sp = chunk; sp <= chunk + strlen(chunk); sp++)
1866     {
1867         if (strlen(chunk) + 1 > buf_size)
1868         {
1869             buf_size = strlen(chunk) + 1;
1870             buf = xrealloc(buf, buf_size);
1871         }
1872 
1873         memset(buf, 0, buf_size);
1874         StringNotMatchingSetCapped(sp, buf_size, "\n", buf);
1875         sp += strlen(buf);
1876 
1877         if (!SelectLine(ctx, buf, a))
1878         {
1879             continue;
1880         }
1881 
1882         if (!preserve_block && IsItemInRegion(ctx, buf, begin_ptr, end_ptr, a->insert_match, pp))
1883         {
1884             RecordNoChange(ctx, pp, a,
1885                            "Promised chunk '%s' exists within selected region of '%s'",
1886                            pp->promiser, edcontext->filename);
1887             continue;
1888         }
1889 
1890         if (InsertLineAtLocation(ctx, buf, start, location, prev, a, pp, edcontext, result))
1891         {
1892             retval = true;
1893         }
1894 
1895         if (preserve_block && a->location.before_after == EDIT_ORDER_BEFORE && location == NULL && prev == NULL)
1896         {
1897             // If we are inserting a preserved block before, need to flip the implied order after the first insertion
1898             // to get the order of the block right
1899             // a.location.before_after = cfe_after;
1900             location = *start;
1901         }
1902 
1903         if (prev)
1904         {
1905             prev = prev->next;
1906         }
1907         else
1908         {
1909             prev = *start;
1910         }
1911 
1912         if (location)
1913         {
1914             location = location->next;
1915         }
1916         else
1917         {
1918             location = *start;
1919         }
1920     }
1921 
1922     free(buf);
1923     return retval;
1924 }
1925 
NeighbourItemMatches(EvalContext * ctx,const Item * file_start,const Item * location,const char * string,EditOrder pos,Rlist * insert_match,const Promise * pp)1926 static bool NeighbourItemMatches(EvalContext *ctx, const Item *file_start, const Item *location, const char *string, EditOrder pos, Rlist *insert_match,
1927                          const Promise *pp)
1928 {
1929 /* Look for a line matching proposed insert before or after location */
1930 
1931     for (const Item *ip = file_start; ip != NULL; ip = ip->next)
1932     {
1933         if (pos == EDIT_ORDER_BEFORE)
1934         {
1935             if ((ip->next) && (ip->next == location))
1936             {
1937                 if (MatchPolicy(ctx, string, ip->name, insert_match, pp))
1938                 {
1939                     return true;
1940                 }
1941                 else
1942                 {
1943                     return false;
1944                 }
1945             }
1946         }
1947 
1948         if (pos == EDIT_ORDER_AFTER)
1949         {
1950             if (ip == location)
1951             {
1952                 if ((ip->next) && (MatchPolicy(ctx, string, ip->next->name, insert_match, pp)))
1953                 {
1954                     return true;
1955                 }
1956                 else
1957                 {
1958                     return false;
1959                 }
1960             }
1961         }
1962     }
1963 
1964     return false;
1965 }
1966 
InsertLineAtLocation(EvalContext * ctx,char * newline,Item ** start,Item * location,Item * prev,const Attributes * a,const Promise * pp,EditContext * edcontext,PromiseResult * result)1967 static bool InsertLineAtLocation(EvalContext *ctx, char *newline, Item **start, Item *location, Item *prev, const Attributes *a,
1968                                 const Promise *pp, EditContext *edcontext, PromiseResult *result)
1969 
1970 /* Check line neighbourhood in whole file to avoid edge effects, iff we are not preseving block structure */
1971 
1972 {   int preserve_block = StringEqual(a->sourcetype, "preserve_block");
1973 
1974     if (!prev)      /* Insert at first line */
1975     {
1976         if (a->location.before_after == EDIT_ORDER_BEFORE)
1977         {
1978             if (*start == NULL)
1979             {
1980                 if (!MakingChanges(ctx, pp, a, result, "insert promised line '%s' into '%s'",
1981                                    newline, edcontext->filename))
1982                 {
1983                     return true;
1984                 }
1985                 else
1986                 {
1987                     PrependItemList(start, newline);
1988                     (edcontext->num_edits)++;
1989                     RecordChange(ctx, pp, a, "Inserted the promised line '%s' into '%s'",
1990                                  newline, edcontext->filename);
1991                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1992                     return true;
1993                 }
1994             }
1995 
1996             if (strcmp((*start)->name, newline) != 0)
1997             {
1998                 if (!MakingChanges(ctx, pp, a, result, "prepend promised line '%s' to '%s'",
1999                                    newline, edcontext->filename))
2000                 {
2001                     return true;
2002                 }
2003                 else
2004                 {
2005                     PrependItemList(start, newline);
2006                     (edcontext->num_edits)++;
2007                     RecordChange(ctx, pp, a, "Prepended the promised line '%s' to %s", newline,
2008                                  edcontext->filename);
2009                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2010                     return true;
2011                 }
2012             }
2013             else
2014             {
2015                 RecordNoChange(ctx, pp, a, "Promised line '%s' exists at start of file '%s'",
2016                                newline, edcontext->filename);
2017                 return false;
2018             }
2019         }
2020     }
2021 
2022     if (a->location.before_after == EDIT_ORDER_BEFORE)
2023     {
2024         if (!preserve_block && NeighbourItemMatches(ctx, *start, location, newline, EDIT_ORDER_BEFORE, a->insert_match, pp))
2025         {
2026             RecordNoChange(ctx, pp, a, "Promised line '%s' exists before locator in '%s'",
2027                            newline, edcontext->filename);
2028             return false;
2029         }
2030         else
2031         {
2032             if (!MakingChanges(ctx, pp, a, result, "insert line '%s' into '%s' before locator",
2033                                newline, edcontext->filename))
2034             {
2035                 return true;
2036             }
2037             else
2038             {
2039                 InsertAfter(start, prev, newline);
2040                 (edcontext->num_edits)++;
2041                 RecordChange(ctx, pp, a, "Inserted the promised line '%s' into '%s' before locator",
2042                              newline, edcontext->filename);
2043                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2044                 return true;
2045             }
2046         }
2047     }
2048     else
2049     {
2050         if (!preserve_block && NeighbourItemMatches(ctx, *start, location, newline, EDIT_ORDER_AFTER, a->insert_match, pp))
2051         {
2052             RecordNoChange(ctx, pp, a, "Promised line '%s' exists after locator in '%s'",
2053                            newline, edcontext->filename);
2054             return false;
2055         }
2056         else
2057         {
2058             if (!MakingChanges(ctx, pp, a, result, "insert line '%s' into '%s' after locator",
2059                                newline, edcontext->filename))
2060             {
2061                 return true;
2062             }
2063             else
2064             {
2065                 InsertAfter(start, location, newline);
2066                 RecordChange(ctx, pp, a, "Inserted the promised line '%s' into '%s' after locator",
2067                              newline, edcontext->filename);
2068                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2069                 (edcontext->num_edits)++;
2070                 return true;
2071             }
2072         }
2073     }
2074 }
2075 
2076 /***************************************************************************/
2077 
EditLineByColumn(EvalContext * ctx,Rlist ** columns,const Attributes * a,const Promise * pp,EditContext * edcontext,PromiseResult * result)2078 static bool EditLineByColumn(EvalContext *ctx, Rlist **columns, const Attributes *a,
2079                              const Promise *pp, EditContext *edcontext, PromiseResult *result)
2080 {
2081     Rlist *rp, *this_column = NULL;
2082     char sep[CF_MAXVARSIZE];
2083     int i, count = 0;
2084     bool retval = false;
2085 
2086 /* Now break up the line into a list - not we never remove an item/column */
2087 
2088     for (rp = *columns; rp != NULL; rp = rp->next)
2089     {
2090         count++;
2091 
2092         if (count == a->column.select_column)
2093         {
2094             Log(LOG_LEVEL_VERBOSE, "Stopped at field %d", count);
2095             break;
2096         }
2097     }
2098 
2099     if (a->column.select_column > count)
2100     {
2101         if (!a->column.extend_columns)
2102         {
2103             RecordInterruption(ctx, pp, a,
2104                                "The file %s has only %d fields, but there is a promise for field %d",
2105                                edcontext->filename, count, a->column.select_column);
2106             *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
2107             return false;
2108         }
2109         else
2110         {
2111             for (i = 0; i < (a->column.select_column - count); i++)
2112             {
2113                 RlistAppendScalar(columns, "");
2114             }
2115 
2116             count = 0;
2117 
2118             for (rp = *columns; rp != NULL; rp = rp->next)
2119             {
2120                 count++;
2121                 if (count == a->column.select_column)
2122                 {
2123                     Log(LOG_LEVEL_VERBOSE, "Stopped at column/field %d", count);
2124                     break;
2125                 }
2126             }
2127         }
2128     }
2129 
2130     if (a->column.value_separator != '\0')
2131     {
2132         /* internal separator, single char so split again */
2133 
2134         if (strstr(RlistScalarValue(rp), a->column.column_value) || strcmp(RlistScalarValue(rp), a->column.column_value) != 0)
2135         {
2136             if (!MakingChanges(ctx, pp, a, result, "edit field '%s' in '%s'",
2137                                a->column.column_value, edcontext->filename))
2138             {
2139                 retval = false;
2140             }
2141             else
2142             {
2143                 this_column = RlistFromSplitString(RlistScalarValue(rp), a->column.value_separator);
2144                 retval = DoEditColumn(&this_column, edcontext, ctx, pp, a, result);
2145                 if (retval)
2146                 {
2147                     (edcontext->num_edits)++;
2148                     free(RlistScalarValue(rp));
2149                     sep[0] = a->column.value_separator;
2150                     sep[1] = '\0';
2151                     rp->val.item = Rlist2String(this_column, sep);
2152                 }
2153             }
2154         }
2155         else
2156         {
2157             retval = false;
2158         }
2159         RlistDestroy(this_column);
2160         return retval;
2161     }
2162     else
2163     {
2164         /* No separator, so we set the whole field to the value */
2165 
2166         if (a->column.column_operation && strcmp(a->column.column_operation, "delete") == 0)
2167         {
2168             if (!MakingChanges(ctx, pp, a, result, "delete field value '%s' in '%s'",
2169                                RlistScalarValue(rp), edcontext->filename))
2170             {
2171                 return false;
2172             }
2173             else
2174             {
2175                 free(rp->val.item);
2176                 rp->val.item = xstrdup("");
2177                 RecordChange(ctx, pp, a, "Deleted column field value '%s' in '%s'",
2178                              RlistScalarValue(rp), edcontext->filename);
2179                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2180                 (edcontext->num_edits)++;
2181                 return true;
2182             }
2183         }
2184         else
2185         {
2186             if (!MakingChanges(ctx, pp, a, result, "set column field value '%s' to '%s' in '%s'",
2187                                RlistScalarValue(rp), a->column.column_value, edcontext->filename))
2188             {
2189                 return false;
2190             }
2191             else
2192             {
2193                 RecordChange(ctx, pp, a, "Set whole column field value '%s' to '%s' in '%s'",
2194                              RlistScalarValue(rp), a->column.column_value, edcontext->filename);
2195                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2196                 free(rp->val.item);
2197                 rp->val.item = xstrdup(a->column.column_value);
2198                 (edcontext->num_edits)++;
2199                 return true;
2200             }
2201         }
2202     }
2203 
2204     RecordNoChange(ctx, pp, a, "No need to edit column field value '%s' in '%s'",
2205                    a->column.column_value, edcontext->filename);
2206 
2207     return false;
2208 }
2209 
2210 /***************************************************************************/
2211 
SelectLine(EvalContext * ctx,const char * line,const Attributes * a)2212 static bool SelectLine(EvalContext *ctx, const char *line, const Attributes *a)
2213 {
2214     Rlist *rp, *c;
2215     int s, e;
2216     char *selector;
2217     const LineSelect line_select = a->line_select;
2218 
2219     if ((c = line_select.startwith_from_list))
2220     {
2221         for (rp = c; rp != NULL; rp = rp->next)
2222         {
2223             selector = RlistScalarValue(rp);
2224 
2225             if (strncmp(selector, line, strlen(selector)) == 0)
2226             {
2227                 return true;
2228             }
2229         }
2230 
2231         return false;
2232     }
2233 
2234     if ((c = line_select.not_startwith_from_list))
2235     {
2236         for (rp = c; rp != NULL; rp = rp->next)
2237         {
2238             selector = RlistScalarValue(rp);
2239 
2240             if (strncmp(selector, line, strlen(selector)) == 0)
2241             {
2242                 return false;
2243             }
2244         }
2245 
2246         return true;
2247     }
2248 
2249     if ((c = line_select.match_from_list))
2250     {
2251         for (rp = c; rp != NULL; rp = rp->next)
2252         {
2253             selector = RlistScalarValue(rp);
2254 
2255             if (FullTextMatch(ctx, selector, line))
2256             {
2257                 return true;
2258             }
2259         }
2260 
2261         return false;
2262     }
2263 
2264     if ((c = line_select.not_match_from_list))
2265     {
2266         for (rp = c; rp != NULL; rp = rp->next)
2267         {
2268             selector = RlistScalarValue(rp);
2269 
2270             if (FullTextMatch(ctx, selector, line))
2271             {
2272                 return false;
2273             }
2274         }
2275 
2276         return true;
2277     }
2278 
2279     if ((c = line_select.contains_from_list))
2280     {
2281         for (rp = c; rp != NULL; rp = rp->next)
2282         {
2283             selector = RlistScalarValue(rp);
2284 
2285             if (BlockTextMatch(ctx, selector, line, &s, &e))
2286             {
2287                 return true;
2288             }
2289         }
2290 
2291         return false;
2292     }
2293 
2294     if ((c = line_select.not_contains_from_list))
2295     {
2296         for (rp = c; rp != NULL; rp = rp->next)
2297         {
2298             selector = RlistScalarValue(rp);
2299 
2300             if (BlockTextMatch(ctx, selector, line, &s, &e))
2301             {
2302                 return false;
2303             }
2304         }
2305 
2306         return true;
2307     }
2308 
2309     return true;
2310 }
2311 
2312 /***************************************************************************/
2313 /* Level                                                                   */
2314 /***************************************************************************/
2315 
DoEditColumn(Rlist ** columns,EditContext * edcontext,EvalContext * ctx,const Promise * pp,const Attributes * a,PromiseResult * result)2316 static bool DoEditColumn(Rlist **columns, EditContext *edcontext,
2317                          EvalContext *ctx, const Promise *pp, const Attributes *a,
2318                          PromiseResult *result)
2319 {
2320     Rlist *rp, *found;
2321     bool retval = false;
2322 
2323     const char *const column_value = a->column.column_value;
2324     const char *const column_operation = a->column.column_operation;
2325 
2326     if (StringEqual(column_operation, "delete"))
2327     {
2328         while ((found = RlistKeyIn(*columns, column_value)))
2329         {
2330             RlistDestroyEntry(columns, found);
2331             RecordChange(ctx, pp, a, "Deleted column field sub-value '%s' in '%s'",
2332                          column_value, edcontext->filename);
2333             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2334             retval = true;
2335         }
2336 
2337         return retval;
2338     }
2339 
2340     if (StringEqual(column_operation, "set"))
2341     {
2342         int length = RlistLen(*columns);
2343         if (length == 1 && strcmp(RlistScalarValue(*columns), column_value) == 0)
2344         {
2345             RecordNoChange(ctx, pp, a, "Field sub-value set as promised");
2346             return false;
2347         }
2348         else if (length == 0 && strcmp("", column_value) == 0)
2349         {
2350             RecordNoChange(ctx, pp, a, "Empty field sub-value set as promised");
2351             return false;
2352         }
2353 
2354         RecordChange(ctx, pp, a, "Set field sub-value '%s' in '%s'", column_value, edcontext->filename);
2355         *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2356         RlistDestroy(*columns);
2357         *columns = NULL;
2358         RlistPrependScalarIdemp(columns, column_value);
2359 
2360         return true;
2361     }
2362 
2363     if (StringEqual(column_operation, "prepend"))
2364     {
2365         if (RlistPrependScalarIdemp(columns, column_value))
2366         {
2367             RecordChange(ctx, pp, a, "Prepended field sub-value '%s' in '%s'",
2368                          column_value, edcontext->filename);
2369             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2370             return true;
2371         }
2372         else
2373         {
2374             RecordNoChange(ctx, pp, a, "Field sub-value '%s' already present in '%s'",
2375                            column_value, edcontext->filename);
2376             return false;
2377         }
2378     }
2379 
2380     if (StringEqual(column_operation, "alphanum"))
2381     {
2382         if (RlistPrependScalarIdemp(columns, column_value))
2383         {
2384             RecordChange(ctx, pp, a, "Inserted field sub-value '%s' in '%s'",
2385                          column_value, edcontext->filename);
2386             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2387             retval = true;
2388         }
2389         else
2390         {
2391             RecordNoChange(ctx, pp, a, "Field sub-value '%s' already present in '%s'",
2392                            column_value, edcontext->filename);
2393             retval = false;
2394         }
2395 
2396         rp = AlphaSortRListNames(*columns);
2397         *columns = rp;
2398         return retval;
2399     }
2400 
2401 /* default operation is append */
2402 
2403     if (RlistAppendScalarIdemp(columns, column_value))
2404     {
2405         RecordChange(ctx, pp, a, "Appended field sub-value '%s' in '%s'",
2406                      column_value, edcontext->filename);
2407         *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2408         return true;
2409     }
2410     else
2411     {
2412         RecordNoChange(ctx, pp, a, "Field sub-value '%s' already present in '%s'",
2413                        column_value, edcontext->filename);
2414         return false;
2415     }
2416 
2417     return false;
2418 }
2419 
2420 /********************************************************************/
2421 
NotAnchored(char * s)2422 static bool NotAnchored(char *s)
2423 {
2424     if (*s != '^')
2425     {
2426         return true;
2427     }
2428 
2429     if (*(s + strlen(s) - 1) != '$')
2430     {
2431         return true;
2432     }
2433 
2434     return false;
2435 }
2436 
2437 /********************************************************************/
2438 
MultiLineString(char * s)2439 static bool MultiLineString(char *s)
2440 {
2441     return (strchr(s, '\n') != NULL);
2442 }
2443