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