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