1 /*
2   Copyright 2021 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", PromiseGetPromiseType(pp)) == 0)
321     {
322         return VerifyClassPromise(ctx, pp, NULL);
323     }
324     else if (strcmp("delete_lines", PromiseGetPromiseType(pp)) == 0)
325     {
326         return VerifyLineDeletions(ctx, pp, edcontext);
327     }
328     else if (strcmp("field_edits", PromiseGetPromiseType(pp)) == 0)
329     {
330         return VerifyColumnEdits(ctx, pp, edcontext);
331     }
332     else if (strcmp("insert_lines", PromiseGetPromiseType(pp)) == 0)
333     {
334         return VerifyLineInsertions(ctx, pp, edcontext);
335     }
336     else if (strcmp("replace_patterns", PromiseGetPromiseType(pp)) == 0)
337     {
338         return VerifyPatterns(ctx, pp, edcontext);
339     }
340     else if (strcmp("reports", PromiseGetPromiseType(pp)) == 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     size_t match_len;
1224     int start_off, end_off, once_only = false, retval = false;
1225     Item *ip;
1226     int notfound = true, cutoff = 1, replaced = false;
1227 
1228     if (StringEqual(a->replace.occurrences, "first"))
1229     {
1230         Log(LOG_LEVEL_WARNING, "Setting replace-occurrences policy to 'first' is not convergent");
1231         once_only = true;
1232     }
1233 
1234     Buffer *replace = BufferNew();
1235     for (ip = file_start; ip != NULL && ip != file_end; ip = ip->next)
1236     {
1237         if (ip->name == NULL)
1238         {
1239             continue;
1240         }
1241 
1242         cutoff = 1;
1243         strlcpy(line_buff, ip->name, sizeof(line_buff));
1244         replaced = false;
1245         match_len = 0;
1246 
1247         while (BlockTextMatch(ctx, pp->promiser, line_buff, &start_off, &end_off))
1248         {
1249             if (match_len == strlen(line_buff))
1250             {
1251                 Log(LOG_LEVEL_VERBOSE, "Improper convergent expression matches defacto convergence, so accepting");
1252                 break;
1253             }
1254 
1255             if (cutoff++ > CF_MAX_REPLACE)
1256             {
1257                 Log(LOG_LEVEL_VERBOSE, "Too many replacements on this line");
1258                 break;
1259             }
1260 
1261             match_len = end_off - start_off;
1262             BufferClear(replace);
1263             ExpandScalar(ctx, PromiseGetBundle(pp)->ns, PromiseGetBundle(pp)->name, a->replace.replace_value, replace);
1264 
1265             Log(LOG_LEVEL_VERBOSE, "Verifying replacement of '%s' with '%s', cutoff %d", pp->promiser, BufferData(replace),
1266                   cutoff);
1267 
1268             // Save portion of line after substitution:
1269             strlcpy(after, line_buff + end_off, sizeof(after));
1270             // TODO: gripe if that truncated !
1271 
1272             // Substitute into line_buff:
1273             snprintf(line_buff + start_off, sizeof(line_buff) - start_off,
1274                      "%s%s", BufferData(replace), after);
1275             // TODO: gripe if that truncated or failed !
1276             notfound = false;
1277             replaced = true;
1278 
1279             if (once_only)
1280             {
1281                 Log(LOG_LEVEL_VERBOSE, "Replace first occurrence only (warning, this is not a convergent policy)");
1282                 break;
1283             }
1284         }
1285 
1286         if (NotAnchored(pp->promiser) && BlockTextMatch(ctx, pp->promiser, line_buff, &start_off, &end_off))
1287         {
1288             RecordInterruption(ctx, pp, a,
1289                                "Promised replacement '%s' on line '%s' for pattern '%s'"
1290                                " is not convergent while editing '%s'"
1291                                " (regular expression matches the replacement string)",
1292                                line_buff, ip->name, pp->promiser, edcontext->filename);
1293             *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
1294             PromiseRef(LOG_LEVEL_ERR, pp);
1295             break;
1296         }
1297 
1298         if (!MakingChanges(ctx, pp, a, result, "replace pattern '%s' in '%s'", pp->promiser,
1299                            edcontext->filename))
1300         {
1301             continue;
1302         }
1303         else if (replaced)
1304         {
1305             free(ip->name);
1306             ip->name = xstrdup(line_buff);
1307             RecordChange(ctx, pp, a, "Replaced pattern '%s' in '%s'", pp->promiser, edcontext->filename);
1308             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1309             (edcontext->num_edits)++;
1310             retval = true;
1311 
1312             Log(LOG_LEVEL_VERBOSE, "cutoff %d, '%s'", cutoff, ip->name);
1313             Log(LOG_LEVEL_VERBOSE, "cutoff %d, '%s'", cutoff, line_buff);
1314 
1315             if (once_only)
1316             {
1317                 Log(LOG_LEVEL_VERBOSE, "Replace first occurrence only (warning, this is not a convergent policy)");
1318                 break;
1319             }
1320 
1321             if (BlockTextMatch(ctx, pp->promiser, ip->name, &start_off, &end_off))
1322             {
1323                 RecordInterruption(ctx, pp, a,
1324                                    "Promised replacement '%s' for pattern '%s' is not properly convergent while editing '%s'"
1325                                    " (pattern still matches the end-state replacement string '%s')",
1326                                    ip->name, pp->promiser, edcontext->filename, line_buff);
1327                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
1328                 PromiseRef(LOG_LEVEL_INFO, pp);
1329             }
1330         }
1331     }
1332 
1333     BufferDestroy(replace);
1334 
1335     if (notfound)
1336     {
1337         RecordNoChange(ctx, pp, a, "No match for pattern '%s' in '%s'", pp->promiser, edcontext->filename);
1338     }
1339 
1340     return retval;
1341 }
1342 
1343 /********************************************************************/
1344 
EditColumns(EvalContext * ctx,Item * file_start,Item * file_end,const Attributes * a,const Promise * pp,EditContext * edcontext,PromiseResult * result)1345 static bool EditColumns(EvalContext *ctx, Item *file_start, Item *file_end, const Attributes *a,
1346                         const Promise *pp, EditContext *edcontext, PromiseResult *result)
1347 {
1348     char separator[CF_MAXVARSIZE];
1349     int s, e;
1350     bool retval = false;
1351     Item *ip;
1352     Rlist *columns = NULL;
1353 
1354     if (!ValidateRegEx(pp->promiser))
1355     {
1356         return false;
1357     }
1358 
1359     bool found_match = false;
1360     for (ip = file_start; ip != file_end; ip = ip->next)
1361     {
1362         if (ip->name == NULL)
1363         {
1364             continue;
1365         }
1366 
1367         if (!FullTextMatch(ctx, pp->promiser, ip->name))
1368         {
1369             continue;
1370         }
1371         else
1372         {
1373             found_match = true;
1374             Log(LOG_LEVEL_VERBOSE, "Matched line '%s'", ip->name);
1375         }
1376 
1377         if (!BlockTextMatch(ctx, a->column.column_separator, ip->name, &s, &e))
1378         {
1379             RecordInterruption(ctx, pp, a, "Field edit, no fields found by promised pattern '%s' in '%s'",
1380                                a->column.column_separator, edcontext->filename);
1381             *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
1382             return false;
1383         }
1384 
1385         if (e - s > CF_MAXVARSIZE / 2)
1386         {
1387             Log(LOG_LEVEL_ERR, "Line split criterion matches a huge part of the line, seems to be in error");
1388             return false;
1389         }
1390 
1391         strlcpy(separator, ip->name + s, e - s + 1);
1392 
1393         columns = RlistFromSplitRegex(ip->name, a->column.column_separator, CF_INFINITY, a->column.blanks_ok);
1394         retval = EditLineByColumn(ctx, &columns, a, pp, edcontext, result);
1395 
1396         if (retval)
1397         {
1398             free(ip->name);
1399             ip->name = Rlist2String(columns, separator);
1400         }
1401 
1402         RlistDestroy(columns);
1403     }
1404 
1405     if (!found_match)
1406     {
1407         RecordFailure(ctx, pp, a,
1408                       "No matched line to edit fields of for pattern '%s' in '%s'",
1409                       pp->promiser, edcontext->filename);
1410         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1411     }
1412 
1413     return retval;
1414 }
1415 
1416 /***************************************************************************/
1417 
SanityCheckInsertions(const Attributes * a)1418 static bool SanityCheckInsertions(const Attributes *a)
1419 {
1420     long not = 0;
1421     long with = 0;
1422     bool ok = true;
1423     Rlist *rp;
1424     InsertMatchType opt;
1425     int exact = false, ignore_something = false;
1426     const bool preserve_block = StringEqual(a->sourcetype, "preserve_block");
1427     const LineSelect line_select = a->line_select;
1428 
1429     if (line_select.startwith_from_list)
1430     {
1431         with++;
1432     }
1433 
1434     if (line_select.not_startwith_from_list)
1435     {
1436         not++;
1437     }
1438 
1439     if (line_select.match_from_list)
1440     {
1441         with++;
1442     }
1443 
1444     if (line_select.not_match_from_list)
1445     {
1446         not++;
1447     }
1448 
1449     if (line_select.contains_from_list)
1450     {
1451         with++;
1452     }
1453 
1454     if (line_select.not_contains_from_list)
1455     {
1456         not++;
1457     }
1458 
1459     if (not > 1)
1460     {
1461         Log(LOG_LEVEL_ERR,
1462               "Line insertion selection promise is meaningless - the alternatives are mutually exclusive (only one is allowed)");
1463         ok = false;
1464     }
1465 
1466     if (with && not)
1467     {
1468         Log(LOG_LEVEL_ERR,
1469               "Line insertion selection promise is meaningless - cannot mix positive and negative constraints");
1470         ok = false;
1471     }
1472 
1473     for (rp = a->insert_match; rp != NULL; rp = rp->next)
1474     {
1475         opt = InsertMatchTypeFromString(RlistScalarValue(rp));
1476 
1477         if (opt == INSERT_MATCH_TYPE_EXACT)
1478         {
1479             exact = true;
1480         }
1481         else
1482         {
1483             ignore_something = true;
1484             if (preserve_block)
1485             {
1486                 Log(LOG_LEVEL_ERR, "Line insertion should not use whitespace policy with preserve_block");
1487                 ok = false;
1488             }
1489         }
1490     }
1491 
1492     if (exact && ignore_something)
1493     {
1494         Log(LOG_LEVEL_ERR,
1495               "Line insertion selection promise is meaningless - cannot mix exact_match with other ignore whitespace options");
1496         ok = false;
1497     }
1498 
1499     return ok;
1500 }
1501 
1502 /***************************************************************************/
1503 
SanityCheckDeletions(const Attributes * a,const Promise * pp)1504 static bool SanityCheckDeletions(const Attributes *a, const Promise *pp)
1505 {
1506     if (MultiLineString(pp->promiser))
1507     {
1508         if (a->not_matching)
1509         {
1510             Log(LOG_LEVEL_ERR,
1511                 "Makes no sense to promise multi-line delete with not_matching. Cannot be satisfied for all lines as a block.");
1512             // FIXME: This function always returns true (!)
1513         }
1514     }
1515 
1516     return true;
1517 }
1518 
1519 /***************************************************************************/
1520 
1521 /* XXX */
MatchPolicy(EvalContext * ctx,const char * camel,const char * haystack,Rlist * insert_match,const Promise * pp)1522 static bool MatchPolicy(EvalContext *ctx, const char *camel, const char *haystack, Rlist *insert_match, const Promise *pp)
1523 {
1524     char *final = NULL;
1525     bool ok      = false;
1526     bool escaped = false;
1527     Item *list = SplitString(camel, '\n');
1528 
1529     //Split into separate lines first
1530     for (Item *ip = list; ip != NULL; ip = ip->next)
1531     {
1532         ok = false;
1533         bool direct_cmp = (strcmp(camel, haystack) == 0);
1534 
1535         final             = xstrdup(ip->name);
1536         size_t final_size = strlen(final) + 1;
1537 
1538         if (insert_match == NULL)
1539         {
1540             // No whitespace policy means exact_match
1541             ok = ok || direct_cmp;
1542             break;
1543         }
1544 
1545         for (Rlist *rp = insert_match; rp != NULL; rp = rp->next)
1546         {
1547             const InsertMatchType opt =
1548                 InsertMatchTypeFromString(RlistScalarValue(rp));
1549 
1550             /* Exact match can be done immediately */
1551 
1552             if (opt == INSERT_MATCH_TYPE_EXACT)
1553             {
1554                 if ((rp->next != NULL) || (rp != insert_match))
1555                 {
1556                     Log(LOG_LEVEL_ERR, "Multiple policies conflict with \"exact_match\", using exact match");
1557                     PromiseRef(LOG_LEVEL_ERR, pp);
1558                 }
1559 
1560                 ok = ok || direct_cmp;
1561                 break;
1562             }
1563 
1564             if (!escaped)
1565             {
1566                 // Need to escape the original string once here in case it contains regex chars when non-exact match
1567                 // Check size of escaped string, and realloc if necessary
1568                 size_t escape_regex_len = EscapeRegexCharsLen(ip->name);
1569                 if (escape_regex_len + 1 > final_size)
1570                 {
1571                     final = xrealloc(final, escape_regex_len + 1);
1572                     final_size = escape_regex_len + 1;
1573                 }
1574 
1575                 EscapeRegexChars(ip->name, final, final_size);
1576                 escaped = true;
1577             }
1578 
1579             if (opt == INSERT_MATCH_TYPE_IGNORE_EMBEDDED)
1580             {
1581                 // Strip initial and final first
1582                 char *firstchar, *lastchar;
1583                 for (firstchar = final; isspace((int)*firstchar); firstchar++);
1584                 for (lastchar = final + strlen(final) - 1; (lastchar > firstchar) && (isspace((int)*lastchar)); lastchar--);
1585 
1586                 // Since we're stripping space and replacing it with \s+, we need to account for that
1587                 // when allocating work
1588                 size_t work_size = final_size + 6;        /* allocated size */
1589                 char  *work      = xcalloc(1, work_size);
1590 
1591                 /* We start only with the terminating '\0'. */
1592                 size_t required_size = 1;
1593 
1594                 for (char *sp = final; *sp != '\0'; sp++)
1595                 {
1596                     char toadd[4];
1597 
1598                     if ((sp > firstchar) && (sp < lastchar))
1599                     {
1600                         if (isspace((int)*sp))
1601                         {
1602                             while (isspace((int)*(sp + 1)))
1603                             {
1604                                 sp++;
1605                             }
1606 
1607                             required_size += 3;
1608                             strcpy(toadd, "\\s+");
1609                         }
1610                         else
1611                         {
1612                             required_size++;
1613                             toadd[0] = *sp;
1614                             toadd[1] = '\0';
1615                         }
1616                     }
1617                     else
1618                     {
1619                         required_size++;
1620                         toadd[0] = *sp;
1621                         toadd[1] = '\0';
1622                     }
1623 
1624                     if (required_size > work_size)
1625                     {
1626                         // Increase by a small amount extra, so we don't
1627                         // reallocate every iteration
1628                         work_size = required_size + 12;
1629                         work = xrealloc(work, work_size);
1630                     }
1631 
1632                     if (strlcat(work, toadd, work_size) >= work_size)
1633                     {
1634                         UnexpectedError("Truncation concatenating '%s' to: %s",
1635                                         toadd, work);
1636                     }
1637                 }
1638 
1639                 // Realloc and retry on truncation
1640                 if (strlcpy(final, work, final_size) >= final_size)
1641                 {
1642                     final = xrealloc(final, work_size);
1643                     final_size = work_size;
1644                     strlcpy(final, work, final_size);
1645                 }
1646 
1647                 free(work);
1648             }
1649             else if (opt == INSERT_MATCH_TYPE_IGNORE_LEADING)
1650             {
1651                 if (strncmp(final, "\\s*", 3) != 0)
1652                 {
1653                     char *sp;
1654                     for (sp = final; isspace((int)*sp); sp++);
1655 
1656                     size_t work_size = final_size + 3;
1657                     char  *work      = xcalloc(1, work_size);
1658                     strcpy(work, sp);
1659 
1660                     int written = snprintf(final, final_size, "\\s*%s",
1661                                            work);
1662                     if (written < 0)
1663                     {
1664                         Log(LOG_LEVEL_ERR,
1665                             "Unexpected failure from snprintf "
1666                             "(%d - %s) on '%s' (MatchPolicy)",
1667                             errno, GetErrorStr(), final);
1668                         return false;
1669                     }
1670                     else if ((size_t) written >= final_size - 1)
1671                     {
1672                         final = xrealloc(final, work_size);
1673                         final_size = work_size;
1674                         written = snprintf(final, final_size, "\\s*%s",
1675                                            work);
1676                         if (written < 0)
1677                         {
1678                             Log(LOG_LEVEL_ERR,
1679                                 "Unexpected failure from snprintf "
1680                                 "(%d - %s) on '%s' (MatchPolicy)",
1681                                 errno, GetErrorStr(), final);
1682                             return false;
1683                         }
1684                     }
1685 
1686                     free(work);
1687                 }
1688             }
1689             else if (opt == INSERT_MATCH_TYPE_IGNORE_TRAILING)
1690             {
1691                 if (strncmp(final + strlen(final) - 4, "\\s*", 3) != 0)
1692                 {
1693                     size_t work_size = final_size + 3;
1694                     char  *work      = xcalloc(1, work_size);
1695                     strcpy(work, final);
1696 
1697                     char *sp;
1698                     for (sp = work + strlen(work) - 1; (sp > work) && (isspace((int)*sp)); sp--);
1699                     *++sp = '\0';
1700                     int written = snprintf(final, final_size, "%s\\s*", work);
1701                     if (written < 0)
1702                     {
1703                         Log(LOG_LEVEL_ERR,
1704                             "Unexpected failure from snprintf "
1705                             "(%d - %s) on '%s' (MatchPolicy)",
1706                             errno, GetErrorStr(), final);
1707                         return false;
1708                     }
1709                     else if ((size_t) written >= final_size - 1)
1710                     {
1711                         final = xrealloc(final, work_size);
1712                         final_size = work_size;
1713                         written = snprintf(final, final_size, "%s\\s*", work);
1714                         if (written < 0)
1715                         {
1716                             Log(LOG_LEVEL_ERR,
1717                                 "Unexpected failure from snprintf "
1718                                 "(%d - %s) on '%s' (MatchPolicy)",
1719                                 errno, GetErrorStr(), final);
1720                             return false;
1721                         }
1722                     }
1723 
1724                     free(work);
1725                 }
1726             }
1727 
1728             ok = ok || (FullTextMatch(ctx, final, haystack));
1729         }
1730 
1731         assert(final_size > strlen(final));
1732         free(final);
1733         final = NULL;
1734         if (!ok)                // All lines in region need to match to avoid insertions
1735         {
1736             break;
1737         }
1738     }
1739 
1740     free(final);
1741     DeleteItemList(list);
1742     return ok;
1743 }
1744 
IsItemInRegion(EvalContext * ctx,const char * item,const Item * begin_ptr,const Item * end_ptr,Rlist * insert_match,const Promise * pp)1745 static bool IsItemInRegion(EvalContext *ctx, const char *item, const Item *begin_ptr, const Item *end_ptr, Rlist *insert_match, const Promise *pp)
1746 {
1747     for (const Item *ip = begin_ptr; ((ip != end_ptr) && (ip != NULL)); ip = ip->next)
1748     {
1749         if (MatchPolicy(ctx, item, ip->name, insert_match, pp))
1750         {
1751             return true;
1752         }
1753     }
1754 
1755     return false;
1756 }
1757 
1758 /***************************************************************************/
1759 
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)1760 static bool InsertFileAtLocation(EvalContext *ctx, Item **start, Item *begin_ptr, Item *end_ptr, Item *location,
1761                                 Item *prev, const Attributes *a, const Promise *pp, EditContext *edcontext, PromiseResult *result)
1762 {
1763     FILE *fin;
1764     bool retval = false;
1765     Item *loc = NULL;
1766     const bool preserve_block = StringEqual(a->sourcetype, "file_preserve_block");
1767 
1768     struct stat sb;
1769     if ((stat(pp->promiser, &sb) == 0) && S_ISDIR(sb.st_mode))
1770     {
1771         RecordInterruption(ctx, pp, a, "Could not insert lines from a directory '%s'", pp->promiser);
1772         *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
1773         return false;
1774     }
1775 
1776     if ((fin = safe_fopen(pp->promiser, "rt")) == NULL)
1777     {
1778         RecordInterruption(ctx, pp, a, "Could not read file '%s'. (fopen: %s)",
1779                            pp->promiser, GetErrorStr());
1780         *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
1781         return false;
1782     }
1783 
1784     size_t buf_size = CF_BUFSIZE;
1785     char *buf = xmalloc(buf_size);
1786     loc = location;
1787     Buffer *exp = BufferNew();
1788 
1789     while (CfReadLine(&buf, &buf_size, fin) != -1)
1790     {
1791         BufferClear(exp);
1792         if (a->expandvars)
1793         {
1794             ExpandScalar(ctx, PromiseGetBundle(pp)->ns, PromiseGetBundle(pp)->name, buf, exp);
1795         }
1796         else
1797         {
1798             BufferAppend(exp, buf, strlen(buf));
1799         }
1800 
1801         if (!SelectLine(ctx, BufferData(exp), a))
1802         {
1803             free(buf);
1804             buf = NULL;
1805             continue;
1806         }
1807 
1808         if (!preserve_block && IsItemInRegion(ctx, BufferData(exp), begin_ptr, end_ptr, a->insert_match, pp))
1809         {
1810             RecordNoChange(ctx, pp, a, "Promised file line '%s' exists within file '%s'",
1811                            BufferData(exp), edcontext->filename);
1812             free(buf);
1813             buf = NULL;
1814             continue;
1815         }
1816 
1817         // Need to call CompoundLine here in case ExpandScalar has inserted \n into a string
1818 
1819         if (InsertCompoundLineAtLocation(ctx, BufferGet(exp), start, begin_ptr, end_ptr,
1820                                          loc, prev, a, pp, edcontext, result))
1821         {
1822             retval = true;
1823         }
1824 
1825         if (preserve_block && !prev)
1826         {
1827             // If we are inserting a preserved block before, need to flip the implied order after the first insertion
1828             // to get the order of the block right
1829             //a->location.before_after = cfe_after;
1830         }
1831 
1832         if (prev)
1833         {
1834             prev = prev->next;
1835         }
1836         else
1837         {
1838             prev = *start;
1839         }
1840 
1841         if (loc)
1842         {
1843             loc = loc->next;
1844         }
1845         else
1846         {
1847             location = *start;
1848         }
1849 
1850         free(buf);
1851         buf = NULL;
1852     }
1853 
1854     if (buf != NULL)
1855     {
1856         free(buf);
1857     }
1858 
1859     if (ferror(fin))
1860     {
1861         if (errno == EISDIR)
1862         {
1863             RecordInterruption(ctx, pp, a, "'%s' is a directory, cannot lines into it", pp->promiser);
1864             *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
1865         }
1866         else
1867         {
1868             UnexpectedError("Failed to read line from stream");
1869             RecordFailure(ctx, pp, a, "Failed to read line from stream");
1870             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1871         }
1872     }
1873 
1874     fclose(fin);
1875     BufferDestroy(exp);
1876     return retval;
1877 }
1878 
1879 /***************************************************************************/
1880 
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)1881 static bool InsertCompoundLineAtLocation(EvalContext *ctx, char *chunk, Item **start, Item *begin_ptr, Item *end_ptr,
1882                                         Item *location, Item *prev, const Attributes *a, const Promise *pp, EditContext *edcontext,
1883                                         PromiseResult *result)
1884 {
1885     bool retval = false;
1886     const char *const type = a->sourcetype;
1887     const bool preserve_all_lines = StringEqual(type, "preserve_all_lines");
1888     const bool preserve_block = type && (preserve_all_lines || strcmp(type, "preserve_block") == 0 || strcmp(type, "file_preserve_block") == 0);
1889 
1890     if (!preserve_all_lines && MatchRegion(ctx, chunk, location, NULL, false))
1891     {
1892         RecordNoChange(ctx, pp, a,
1893                        "Promised chunk '%s' exists within selected region of %s (promise kept)",
1894                        pp->promiser, edcontext->filename);
1895         return false;
1896     }
1897 
1898     // Iterate over any lines within the chunk
1899 
1900     char *buf = NULL;
1901     size_t buf_size = 0;
1902     for (char *sp = chunk; sp <= chunk + strlen(chunk); sp++)
1903     {
1904         if (strlen(chunk) + 1 > buf_size)
1905         {
1906             buf_size = strlen(chunk) + 1;
1907             buf = xrealloc(buf, buf_size);
1908         }
1909 
1910         memset(buf, 0, buf_size);
1911         StringNotMatchingSetCapped(sp, buf_size, "\n", buf);
1912         sp += strlen(buf);
1913 
1914         if (!SelectLine(ctx, buf, a))
1915         {
1916             continue;
1917         }
1918 
1919         if (!preserve_block && IsItemInRegion(ctx, buf, begin_ptr, end_ptr, a->insert_match, pp))
1920         {
1921             RecordNoChange(ctx, pp, a,
1922                            "Promised chunk '%s' exists within selected region of '%s'",
1923                            pp->promiser, edcontext->filename);
1924             continue;
1925         }
1926 
1927         if (InsertLineAtLocation(ctx, buf, start, location, prev, a, pp, edcontext, result))
1928         {
1929             retval = true;
1930         }
1931 
1932         if (preserve_block && a->location.before_after == EDIT_ORDER_BEFORE && location == NULL && prev == NULL)
1933         {
1934             // If we are inserting a preserved block before, need to flip the implied order after the first insertion
1935             // to get the order of the block right
1936             // a.location.before_after = cfe_after;
1937             location = *start;
1938         }
1939 
1940         if (prev)
1941         {
1942             prev = prev->next;
1943         }
1944         else
1945         {
1946             prev = *start;
1947         }
1948 
1949         if (location)
1950         {
1951             location = location->next;
1952         }
1953         else
1954         {
1955             location = *start;
1956         }
1957     }
1958 
1959     free(buf);
1960     return retval;
1961 }
1962 
NeighbourItemMatches(EvalContext * ctx,const Item * file_start,const Item * location,const char * string,EditOrder pos,Rlist * insert_match,const Promise * pp)1963 static bool NeighbourItemMatches(EvalContext *ctx, const Item *file_start, const Item *location, const char *string, EditOrder pos, Rlist *insert_match,
1964                          const Promise *pp)
1965 {
1966 /* Look for a line matching proposed insert before or after location */
1967 
1968     for (const Item *ip = file_start; ip != NULL; ip = ip->next)
1969     {
1970         if (pos == EDIT_ORDER_BEFORE)
1971         {
1972             if ((ip->next) && (ip->next == location))
1973             {
1974                 if (MatchPolicy(ctx, string, ip->name, insert_match, pp))
1975                 {
1976                     return true;
1977                 }
1978                 else
1979                 {
1980                     return false;
1981                 }
1982             }
1983         }
1984 
1985         if (pos == EDIT_ORDER_AFTER)
1986         {
1987             if (ip == location)
1988             {
1989                 if ((ip->next) && (MatchPolicy(ctx, string, ip->next->name, insert_match, pp)))
1990                 {
1991                     return true;
1992                 }
1993                 else
1994                 {
1995                     return false;
1996                 }
1997             }
1998         }
1999     }
2000 
2001     return false;
2002 }
2003 
InsertLineAtLocation(EvalContext * ctx,char * newline,Item ** start,Item * location,Item * prev,const Attributes * a,const Promise * pp,EditContext * edcontext,PromiseResult * result)2004 static bool InsertLineAtLocation(EvalContext *ctx, char *newline, Item **start, Item *location, Item *prev, const Attributes *a,
2005                                 const Promise *pp, EditContext *edcontext, PromiseResult *result)
2006 
2007 /* Check line neighbourhood in whole file to avoid edge effects, iff we are not preseving block structure */
2008 
2009 {   int preserve_block = StringEqual(a->sourcetype, "preserve_block");
2010 
2011     if (!prev)      /* Insert at first line */
2012     {
2013         if (a->location.before_after == EDIT_ORDER_BEFORE)
2014         {
2015             if (*start == NULL)
2016             {
2017                 if (!MakingChanges(ctx, pp, a, result, "insert promised line '%s' into '%s'",
2018                                    newline, edcontext->filename))
2019                 {
2020                     return true;
2021                 }
2022                 else
2023                 {
2024                     PrependItemList(start, newline);
2025                     (edcontext->num_edits)++;
2026                     RecordChange(ctx, pp, a, "Inserted the promised line '%s' into '%s'",
2027                                  newline, edcontext->filename);
2028                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2029                     return true;
2030                 }
2031             }
2032 
2033             if (strcmp((*start)->name, newline) != 0)
2034             {
2035                 if (!MakingChanges(ctx, pp, a, result, "prepend promised line '%s' to '%s'",
2036                                    newline, edcontext->filename))
2037                 {
2038                     return true;
2039                 }
2040                 else
2041                 {
2042                     PrependItemList(start, newline);
2043                     (edcontext->num_edits)++;
2044                     RecordChange(ctx, pp, a, "Prepended the promised line '%s' to %s", newline,
2045                                  edcontext->filename);
2046                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2047                     return true;
2048                 }
2049             }
2050             else
2051             {
2052                 RecordNoChange(ctx, pp, a, "Promised line '%s' exists at start of file '%s'",
2053                                newline, edcontext->filename);
2054                 return false;
2055             }
2056         }
2057     }
2058 
2059     if (a->location.before_after == EDIT_ORDER_BEFORE)
2060     {
2061         if (!preserve_block && NeighbourItemMatches(ctx, *start, location, newline, EDIT_ORDER_BEFORE, a->insert_match, pp))
2062         {
2063             RecordNoChange(ctx, pp, a, "Promised line '%s' exists before locator in '%s'",
2064                            newline, edcontext->filename);
2065             return false;
2066         }
2067         else
2068         {
2069             if (!MakingChanges(ctx, pp, a, result, "insert line '%s' into '%s' before locator",
2070                                newline, edcontext->filename))
2071             {
2072                 return true;
2073             }
2074             else
2075             {
2076                 InsertAfter(start, prev, newline);
2077                 (edcontext->num_edits)++;
2078                 RecordChange(ctx, pp, a, "Inserted the promised line '%s' into '%s' before locator",
2079                              newline, edcontext->filename);
2080                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2081                 return true;
2082             }
2083         }
2084     }
2085     else
2086     {
2087         if (!preserve_block && NeighbourItemMatches(ctx, *start, location, newline, EDIT_ORDER_AFTER, a->insert_match, pp))
2088         {
2089             RecordNoChange(ctx, pp, a, "Promised line '%s' exists after locator in '%s'",
2090                            newline, edcontext->filename);
2091             return false;
2092         }
2093         else
2094         {
2095             if (!MakingChanges(ctx, pp, a, result, "insert line '%s' into '%s' after locator",
2096                                newline, edcontext->filename))
2097             {
2098                 return true;
2099             }
2100             else
2101             {
2102                 InsertAfter(start, location, newline);
2103                 RecordChange(ctx, pp, a, "Inserted the promised line '%s' into '%s' after locator",
2104                              newline, edcontext->filename);
2105                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2106                 (edcontext->num_edits)++;
2107                 return true;
2108             }
2109         }
2110     }
2111 }
2112 
2113 /***************************************************************************/
2114 
EditLineByColumn(EvalContext * ctx,Rlist ** columns,const Attributes * a,const Promise * pp,EditContext * edcontext,PromiseResult * result)2115 static bool EditLineByColumn(EvalContext *ctx, Rlist **columns, const Attributes *a,
2116                              const Promise *pp, EditContext *edcontext, PromiseResult *result)
2117 {
2118     Rlist *rp, *this_column = NULL;
2119     char sep[CF_MAXVARSIZE];
2120     int i, count = 0;
2121     bool retval = false;
2122 
2123 /* Now break up the line into a list - not we never remove an item/column */
2124 
2125     for (rp = *columns; rp != NULL; rp = rp->next)
2126     {
2127         count++;
2128 
2129         if (count == a->column.select_column)
2130         {
2131             Log(LOG_LEVEL_VERBOSE, "Stopped at field %d", count);
2132             break;
2133         }
2134     }
2135 
2136     if (a->column.select_column > count)
2137     {
2138         if (!a->column.extend_columns)
2139         {
2140             RecordInterruption(ctx, pp, a,
2141                                "The file %s has only %d fields, but there is a promise for field %d",
2142                                edcontext->filename, count, a->column.select_column);
2143             *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
2144             return false;
2145         }
2146         else
2147         {
2148             for (i = 0; i < (a->column.select_column - count); i++)
2149             {
2150                 RlistAppendScalar(columns, "");
2151             }
2152 
2153             count = 0;
2154 
2155             for (rp = *columns; rp != NULL; rp = rp->next)
2156             {
2157                 count++;
2158                 if (count == a->column.select_column)
2159                 {
2160                     Log(LOG_LEVEL_VERBOSE, "Stopped at column/field %d", count);
2161                     break;
2162                 }
2163             }
2164         }
2165     }
2166 
2167     if (a->column.value_separator != '\0')
2168     {
2169         /* internal separator, single char so split again */
2170 
2171         if (strstr(RlistScalarValue(rp), a->column.column_value) || strcmp(RlistScalarValue(rp), a->column.column_value) != 0)
2172         {
2173             if (!MakingChanges(ctx, pp, a, result, "edit field '%s' in '%s'",
2174                                a->column.column_value, edcontext->filename))
2175             {
2176                 retval = false;
2177             }
2178             else
2179             {
2180                 this_column = RlistFromSplitString(RlistScalarValue(rp), a->column.value_separator);
2181                 retval = DoEditColumn(&this_column, edcontext, ctx, pp, a, result);
2182                 if (retval)
2183                 {
2184                     (edcontext->num_edits)++;
2185                     free(RlistScalarValue(rp));
2186                     sep[0] = a->column.value_separator;
2187                     sep[1] = '\0';
2188                     rp->val.item = Rlist2String(this_column, sep);
2189                 }
2190             }
2191         }
2192         else
2193         {
2194             retval = false;
2195         }
2196         RlistDestroy(this_column);
2197         return retval;
2198     }
2199     else
2200     {
2201         /* No separator, so we set the whole field to the value */
2202 
2203         if (a->column.column_operation && strcmp(a->column.column_operation, "delete") == 0)
2204         {
2205             if (!MakingChanges(ctx, pp, a, result, "delete field value '%s' in '%s'",
2206                                RlistScalarValue(rp), edcontext->filename))
2207             {
2208                 return false;
2209             }
2210             else
2211             {
2212                 free(rp->val.item);
2213                 rp->val.item = xstrdup("");
2214                 RecordChange(ctx, pp, a, "Deleted column field value '%s' in '%s'",
2215                              RlistScalarValue(rp), edcontext->filename);
2216                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2217                 (edcontext->num_edits)++;
2218                 return true;
2219             }
2220         }
2221         else
2222         {
2223             if (!MakingChanges(ctx, pp, a, result, "set column field value '%s' to '%s' in '%s'",
2224                                RlistScalarValue(rp), a->column.column_value, edcontext->filename))
2225             {
2226                 return false;
2227             }
2228             else
2229             {
2230                 RecordChange(ctx, pp, a, "Set whole column field value '%s' to '%s' in '%s'",
2231                              RlistScalarValue(rp), a->column.column_value, edcontext->filename);
2232                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2233                 free(rp->val.item);
2234                 rp->val.item = xstrdup(a->column.column_value);
2235                 (edcontext->num_edits)++;
2236                 return true;
2237             }
2238         }
2239     }
2240 
2241     RecordNoChange(ctx, pp, a, "No need to edit column field value '%s' in '%s'",
2242                    a->column.column_value, edcontext->filename);
2243 
2244     return false;
2245 }
2246 
2247 /***************************************************************************/
2248 
SelectLine(EvalContext * ctx,const char * line,const Attributes * a)2249 static bool SelectLine(EvalContext *ctx, const char *line, const Attributes *a)
2250 {
2251     Rlist *rp, *c;
2252     int s, e;
2253     char *selector;
2254     const LineSelect line_select = a->line_select;
2255 
2256     if ((c = line_select.startwith_from_list))
2257     {
2258         for (rp = c; rp != NULL; rp = rp->next)
2259         {
2260             selector = RlistScalarValue(rp);
2261 
2262             if (strncmp(selector, line, strlen(selector)) == 0)
2263             {
2264                 return true;
2265             }
2266         }
2267 
2268         return false;
2269     }
2270 
2271     if ((c = line_select.not_startwith_from_list))
2272     {
2273         for (rp = c; rp != NULL; rp = rp->next)
2274         {
2275             selector = RlistScalarValue(rp);
2276 
2277             if (strncmp(selector, line, strlen(selector)) == 0)
2278             {
2279                 return false;
2280             }
2281         }
2282 
2283         return true;
2284     }
2285 
2286     if ((c = line_select.match_from_list))
2287     {
2288         for (rp = c; rp != NULL; rp = rp->next)
2289         {
2290             selector = RlistScalarValue(rp);
2291 
2292             if (FullTextMatch(ctx, selector, line))
2293             {
2294                 return true;
2295             }
2296         }
2297 
2298         return false;
2299     }
2300 
2301     if ((c = line_select.not_match_from_list))
2302     {
2303         for (rp = c; rp != NULL; rp = rp->next)
2304         {
2305             selector = RlistScalarValue(rp);
2306 
2307             if (FullTextMatch(ctx, selector, line))
2308             {
2309                 return false;
2310             }
2311         }
2312 
2313         return true;
2314     }
2315 
2316     if ((c = line_select.contains_from_list))
2317     {
2318         for (rp = c; rp != NULL; rp = rp->next)
2319         {
2320             selector = RlistScalarValue(rp);
2321 
2322             if (BlockTextMatch(ctx, selector, line, &s, &e))
2323             {
2324                 return true;
2325             }
2326         }
2327 
2328         return false;
2329     }
2330 
2331     if ((c = line_select.not_contains_from_list))
2332     {
2333         for (rp = c; rp != NULL; rp = rp->next)
2334         {
2335             selector = RlistScalarValue(rp);
2336 
2337             if (BlockTextMatch(ctx, selector, line, &s, &e))
2338             {
2339                 return false;
2340             }
2341         }
2342 
2343         return true;
2344     }
2345 
2346     return true;
2347 }
2348 
2349 /***************************************************************************/
2350 /* Level                                                                   */
2351 /***************************************************************************/
2352 
DoEditColumn(Rlist ** columns,EditContext * edcontext,EvalContext * ctx,const Promise * pp,const Attributes * a,PromiseResult * result)2353 static bool DoEditColumn(Rlist **columns, EditContext *edcontext,
2354                          EvalContext *ctx, const Promise *pp, const Attributes *a,
2355                          PromiseResult *result)
2356 {
2357     Rlist *rp, *found;
2358     bool retval = false;
2359 
2360     const char *const column_value = a->column.column_value;
2361     const char *const column_operation = a->column.column_operation;
2362 
2363     if (StringEqual(column_operation, "delete"))
2364     {
2365         while ((found = RlistKeyIn(*columns, column_value)))
2366         {
2367             RlistDestroyEntry(columns, found);
2368             RecordChange(ctx, pp, a, "Deleted column field sub-value '%s' in '%s'",
2369                          column_value, edcontext->filename);
2370             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2371             retval = true;
2372         }
2373 
2374         return retval;
2375     }
2376 
2377     if (StringEqual(column_operation, "set"))
2378     {
2379         int length = RlistLen(*columns);
2380         if (length == 1 && strcmp(RlistScalarValue(*columns), column_value) == 0)
2381         {
2382             RecordNoChange(ctx, pp, a, "Field sub-value set as promised");
2383             return false;
2384         }
2385         else if (length == 0 && strcmp("", column_value) == 0)
2386         {
2387             RecordNoChange(ctx, pp, a, "Empty field sub-value set as promised");
2388             return false;
2389         }
2390 
2391         RecordChange(ctx, pp, a, "Set field sub-value '%s' in '%s'", column_value, edcontext->filename);
2392         *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2393         RlistDestroy(*columns);
2394         *columns = NULL;
2395         RlistPrependScalarIdemp(columns, column_value);
2396 
2397         return true;
2398     }
2399 
2400     if (StringEqual(column_operation, "prepend"))
2401     {
2402         if (RlistPrependScalarIdemp(columns, column_value))
2403         {
2404             RecordChange(ctx, pp, a, "Prepended field sub-value '%s' in '%s'",
2405                          column_value, edcontext->filename);
2406             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2407             return true;
2408         }
2409         else
2410         {
2411             RecordNoChange(ctx, pp, a, "Field sub-value '%s' already present in '%s'",
2412                            column_value, edcontext->filename);
2413             return false;
2414         }
2415     }
2416 
2417     if (StringEqual(column_operation, "alphanum"))
2418     {
2419         if (RlistPrependScalarIdemp(columns, column_value))
2420         {
2421             RecordChange(ctx, pp, a, "Inserted field sub-value '%s' in '%s'",
2422                          column_value, edcontext->filename);
2423             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2424             retval = true;
2425         }
2426         else
2427         {
2428             RecordNoChange(ctx, pp, a, "Field sub-value '%s' already present in '%s'",
2429                            column_value, edcontext->filename);
2430             retval = false;
2431         }
2432 
2433         rp = AlphaSortRListNames(*columns);
2434         *columns = rp;
2435         return retval;
2436     }
2437 
2438 /* default operation is append */
2439 
2440     if (RlistAppendScalarIdemp(columns, column_value))
2441     {
2442         RecordChange(ctx, pp, a, "Appended field sub-value '%s' in '%s'",
2443                      column_value, edcontext->filename);
2444         *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
2445         return true;
2446     }
2447     else
2448     {
2449         RecordNoChange(ctx, pp, a, "Field sub-value '%s' already present in '%s'",
2450                        column_value, edcontext->filename);
2451         return false;
2452     }
2453 
2454     return false;
2455 }
2456 
2457 /********************************************************************/
2458 
NotAnchored(char * s)2459 static bool NotAnchored(char *s)
2460 {
2461     if (*s != '^')
2462     {
2463         return true;
2464     }
2465 
2466     if (*(s + strlen(s) - 1) != '$')
2467     {
2468         return true;
2469     }
2470 
2471     return false;
2472 }
2473 
2474 /********************************************************************/
2475 
MultiLineString(char * s)2476 static bool MultiLineString(char *s)
2477 {
2478     return (strchr(s, '\n') != NULL);
2479 }
2480