xref: /netbsd-src/external/ibm-public/postfix/dist/src/postconf/postconf_edit.c (revision c48c605c14fd8622b523d1d6a3f0c0bad133ea89)
1 /*	$NetBSD: postconf_edit.c,v 1.3 2023/12/23 20:30:44 christos Exp $	*/
2 
3 /*++
4 /* NAME
5 /*	postconf_edit 3
6 /* SUMMARY
7 /*	edit main.cf or master.cf
8 /* SYNOPSIS
9 /*	#include <postconf.h>
10 /*
11 /*	void	pcf_edit_main(mode, argc, argv)
12 /*	int	mode;
13 /*	int	argc;
14 /*	char	**argv;
15 /*
16 /*	void	pcf_edit_master(mode, argc, argv)
17 /*	int	mode;
18 /*	int	argc;
19 /*	char	**argv;
20 /* DESCRIPTION
21 /*	pcf_edit_main() edits the \fBmain.cf\fR configuration file.
22 /*	It replaces or adds parameter settings given as "\fIname=value\fR"
23 /*	pairs given on the command line, or removes parameter
24 /*	settings given as "\fIname\fR" on the command line.  The
25 /*	file is copied to a temporary file then renamed into place.
26 /*
27 /*	pcf_edit_master() edits the \fBmaster.cf\fR configuration
28 /*	file.  The file is copied to a temporary file then renamed
29 /*	into place. Depending on the flags in \fBmode\fR:
30 /* .IP PCF_MASTER_ENTRY
31 /*	With PCF_EDIT_CONF, pcf_edit_master() replaces or adds
32 /*	entire master.cf entries, specified on the command line as
33 /*	"\fIname/type = name type private unprivileged chroot wakeup
34 /*	process_limit command...\fR".
35 /*
36 /*	With PCF_EDIT_EXCL or PCF_COMMENT_OUT, pcf_edit_master()
37 /*	removes or comments out entries specified on the command
38 /*	line as "\fIname/type\fR.
39 /* .IP PCF_MASTER_FLD
40 /*	With PCF_EDIT_CONF, pcf_edit_master() replaces the value
41 /*	of specific service attributes, specified on the command
42 /*	line as "\fIname/type/attribute = value\fR".
43 /* .IP PCF_MASTER_PARAM
44 /*	With PCF_EDIT_CONF, pcf_edit_master() replaces or adds the
45 /*	value of service parameters, specified on the command line
46 /*	as "\fIname/type/parameter = value\fR".
47 /*
48 /*	With PCF_EDIT_EXCL, pcf_edit_master() removes service
49 /*	parameters specified on the command line as "\fIparametername\fR".
50 /* DIAGNOSTICS
51 /*	Problems are reported to the standard error stream.
52 /* FILES
53 /*	/etc/postfix/main.cf, Postfix configuration parameters
54 /*	/etc/postfix/main.cf.tmp, temporary name
55 /*	/etc/postfix/master.cf, Postfix configuration parameters
56 /*	/etc/postfix/master.cf.tmp, temporary name
57 /* LICENSE
58 /* .ad
59 /* .fi
60 /*	The Secure Mailer license must be distributed with this software.
61 /* AUTHOR(S)
62 /*	Wietse Venema
63 /*	IBM T.J. Watson Research
64 /*	P.O. Box 704
65 /*	Yorktown Heights, NY 10598, USA
66 /*--*/
67 
68 /* System library. */
69 
70 #include <sys_defs.h>
71 #include <string.h>
72 #include <ctype.h>
73 
74 /* Utility library. */
75 
76 #include <msg.h>
77 #include <mymalloc.h>
78 #include <htable.h>
79 #include <vstring.h>
80 #include <vstring_vstream.h>
81 #include <edit_file.h>
82 #include <readlline.h>
83 #include <stringops.h>
84 #include <split_at.h>
85 
86 /* Global library. */
87 
88 #include <mail_params.h>
89 
90 /* Application-specific. */
91 
92 #include <postconf.h>
93 
94 #define STR(x) vstring_str(x)
95 
96 /* pcf_find_cf_info - pass-through non-content line, return content or null */
97 
pcf_find_cf_info(VSTRING * buf,VSTREAM * dst)98 static char *pcf_find_cf_info(VSTRING *buf, VSTREAM *dst)
99 {
100     char   *cp;
101 
102     for (cp = STR(buf); ISSPACE(*cp) /* including newline */ ; cp++)
103 	 /* void */ ;
104     /* Pass-through comment, all-whitespace, or empty line. */
105     if (*cp == '#' || *cp == 0) {
106 	vstream_fputs(STR(buf), dst);
107 	return (0);
108     } else {
109 	return (cp);
110     }
111 }
112 
113 /* pcf_next_cf_line - return next content line, pass non-content */
114 
pcf_next_cf_line(VSTRING * buf,VSTREAM * src,VSTREAM * dst,int * lineno)115 static char *pcf_next_cf_line(VSTRING *buf, VSTREAM *src, VSTREAM *dst, int *lineno)
116 {
117     char   *cp;
118 
119     while (vstring_get(buf, src) != VSTREAM_EOF) {
120 	if (lineno)
121 	    *lineno += 1;
122 	if ((cp = pcf_find_cf_info(buf, dst)) != 0)
123 	    return (cp);
124     }
125     return (0);
126 }
127 
128 /* pcf_gobble_cf_line - accumulate multi-line content, pass non-content */
129 
pcf_gobble_cf_line(VSTRING * full_entry_buf,VSTRING * line_buf,VSTREAM * src,VSTREAM * dst,int * lineno)130 static void pcf_gobble_cf_line(VSTRING *full_entry_buf, VSTRING *line_buf,
131 			            VSTREAM *src, VSTREAM *dst, int *lineno)
132 {
133     int     ch;
134 
135     vstring_strcpy(full_entry_buf, STR(line_buf));
136     for (;;) {
137 	if ((ch = VSTREAM_GETC(src)) != VSTREAM_EOF)
138 	    vstream_ungetc(src, ch);
139 	if ((ch != '#' && !ISSPACE(ch))
140 	    || vstring_get(line_buf, src) == VSTREAM_EOF)
141 	    break;
142 	lineno += 1;
143 	if (pcf_find_cf_info(line_buf, dst))
144 	    vstring_strcat(full_entry_buf, STR(line_buf));
145     }
146 }
147 
148 /* pcf_edit_main - edit main.cf file */
149 
pcf_edit_main(int mode,int argc,char ** argv)150 void    pcf_edit_main(int mode, int argc, char **argv)
151 {
152     const char *path;
153     EDIT_FILE *ep;
154     VSTREAM *src;
155     VSTREAM *dst;
156     VSTRING *buf = vstring_alloc(100);
157     VSTRING *key = vstring_alloc(10);
158     char   *cp;
159     char   *pattern;
160     char   *edit_value;
161     HTABLE *table;
162     struct cvalue {
163 	char   *value;
164 	int     found;
165     };
166     struct cvalue *cvalue;
167     HTABLE_INFO **ht_info;
168     HTABLE_INFO **ht;
169     int     interesting;
170     const char *err;
171 
172     /*
173      * Store command-line parameters for quick lookup.
174      */
175     table = htable_create(argc);
176     while ((cp = *argv++) != 0) {
177 	if (strchr(cp, '\n') != 0)
178 	    msg_fatal("-e, -X, or -# accepts no multi-line input");
179 	while (ISSPACE(*cp))
180 	    cp++;
181 	if (*cp == '#')
182 	    msg_fatal("-e, -X, or -# accepts no comment input");
183 	if (mode & PCF_EDIT_CONF) {
184 	    if ((err = split_nameval(cp, &pattern, &edit_value)) != 0)
185 		msg_fatal("%s: \"%s\"", err, cp);
186 	} else if (mode & (PCF_COMMENT_OUT | PCF_EDIT_EXCL)) {
187 	    if (*cp == 0)
188 		msg_fatal("-X or -# requires non-blank parameter names");
189 	    if (strchr(cp, '=') != 0)
190 		msg_fatal("-X or -# requires parameter names without value");
191 	    pattern = cp;
192 	    trimblanks(pattern, 0);
193 	    edit_value = 0;
194 	} else {
195 	    msg_panic("pcf_edit_main: unknown mode %d", mode);
196 	}
197 	if ((cvalue = htable_find(table, pattern)) != 0) {
198 	    msg_warn("ignoring earlier request: '%s = %s'",
199 		     pattern, cvalue->value);
200 	    htable_delete(table, pattern, myfree);
201 	}
202 	cvalue = (struct cvalue *) mymalloc(sizeof(*cvalue));
203 	cvalue->value = edit_value;
204 	cvalue->found = 0;
205 	htable_enter(table, pattern, (void *) cvalue);
206     }
207 
208     /*
209      * Open a temp file for the result. This uses a deterministic name so we
210      * don't leave behind thrash with random names.
211      */
212     path = pcf_get_main_path();
213     if ((ep = edit_file_open(path, O_CREAT | O_WRONLY, 0644)) == 0)
214 	msg_fatal("open %s%s: %m", path, EDIT_FILE_SUFFIX);
215     dst = ep->tmp_fp;
216 
217     /*
218      * Open the original file for input.
219      */
220     if ((src = vstream_fopen(path, O_RDONLY, 0)) == 0) {
221 	/* OK to delete, since we control the temp file name exclusively. */
222 	(void) unlink(ep->tmp_path);
223 	msg_fatal("open %s for reading: %m", path);
224     }
225 
226     /*
227      * Copy original file to temp file, while replacing parameters on the
228      * fly. Issue warnings for names found multiple times.
229      */
230 #define STR(x) vstring_str(x)
231 
232     interesting = 0;
233     while ((cp = pcf_next_cf_line(buf, src, dst, (int *) 0)) != 0) {
234 	/* Copy, skip or replace continued text. */
235 	if (cp > STR(buf)) {
236 	    if (interesting == 0)
237 		vstream_fputs(STR(buf), dst);
238 	    else if (mode & PCF_COMMENT_OUT)
239 		vstream_fprintf(dst, "#%s", STR(buf));
240 	}
241 	/* Copy or replace start of logical line. */
242 	else {
243 	    vstring_strncpy(key, cp, strcspn(cp, CHARS_SPACE "="));
244 	    cvalue = (struct cvalue *) htable_find(table, STR(key));
245 	    if ((interesting = !!cvalue) != 0) {
246 		if (cvalue->found++ == 1)
247 		    msg_warn("%s: multiple entries for \"%s\"", path, STR(key));
248 		if (mode & PCF_EDIT_CONF)
249 		    vstream_fprintf(dst, "%s = %s\n", STR(key), cvalue->value);
250 		else if (mode & PCF_COMMENT_OUT)
251 		    vstream_fprintf(dst, "#%s", cp);
252 	    } else {
253 		vstream_fputs(STR(buf), dst);
254 	    }
255 	}
256     }
257 
258     /*
259      * Generate new entries for parameters that were not found.
260      */
261     if (mode & PCF_EDIT_CONF) {
262 	for (ht_info = ht = htable_list(table); *ht; ht++) {
263 	    cvalue = (struct cvalue *) ht[0]->value;
264 	    if (cvalue->found == 0)
265 		vstream_fprintf(dst, "%s = %s\n", ht[0]->key, cvalue->value);
266 	}
267 	myfree((void *) ht_info);
268     }
269 
270     /*
271      * When all is well, rename the temp file to the original one.
272      */
273     if (vstream_fclose(src))
274 	msg_fatal("read %s: %m", path);
275     if (edit_file_close(ep) != 0)
276 	msg_fatal("close %s%s: %m", path, EDIT_FILE_SUFFIX);
277 
278     /*
279      * Cleanup.
280      */
281     vstring_free(buf);
282     vstring_free(key);
283     htable_free(table, myfree);
284 }
285 
286  /*
287   * Data structure to hold a master.cf edit request.
288   */
289 typedef struct {
290     int     match_count;		/* hit count */
291     const char *raw_text;		/* unparsed command-line argument */
292     char   *parsed_text;		/* destructive parse */
293     ARGV   *service_pattern;		/* service name, type, ... */
294     int     field_number;		/* attribute field number */
295     const char *param_pattern;		/* parameter name */
296     char   *edit_value;			/* value substring */
297 } PCF_MASTER_EDIT_REQ;
298 
299 /* pcf_edit_master - edit master.cf file */
300 
pcf_edit_master(int mode,int argc,char ** argv)301 void    pcf_edit_master(int mode, int argc, char **argv)
302 {
303     const char *myname = "pcf_edit_master";
304     const char *path;
305     EDIT_FILE *ep;
306     VSTREAM *src;
307     VSTREAM *dst;
308     VSTRING *line_buf = vstring_alloc(100);
309     VSTRING *parse_buf = vstring_alloc(100);
310     int     lineno;
311     PCF_MASTER_ENT *new_entry;
312     VSTRING *full_entry_buf = vstring_alloc(100);
313     char   *cp;
314     char   *pattern;
315     int     service_name_type_matched;
316     const char *err;
317     PCF_MASTER_EDIT_REQ *edit_reqs;
318     PCF_MASTER_EDIT_REQ *req;
319     int     num_reqs = argc;
320     const char *edit_opts = "-Me, -Fe, -Pe, -X, or -#";
321     char   *service_name;
322     char   *service_type;
323 
324     /*
325      * Sanity check.
326      */
327     if (num_reqs <= 0)
328 	msg_panic("%s: empty argument list", myname);
329 
330     /*
331      * Preprocessing: split pattern=value, then split the pattern components.
332      */
333     edit_reqs = (PCF_MASTER_EDIT_REQ *) mymalloc(sizeof(*edit_reqs) * num_reqs);
334     for (req = edit_reqs; *argv != 0; req++, argv++) {
335 	req->match_count = 0;
336 	req->raw_text = *argv;
337 	cp = req->parsed_text = mystrdup(req->raw_text);
338 	if (strchr(cp, '\n') != 0)
339 	    msg_fatal("%s accept no multi-line input", edit_opts);
340 	while (ISSPACE(*cp))
341 	    cp++;
342 	if (*cp == '#')
343 	    msg_fatal("%s accept no comment input", edit_opts);
344 	/* Separate the pattern from the value. */
345 	if (mode & PCF_EDIT_CONF) {
346 	    if ((err = split_nameval(cp, &pattern, &req->edit_value)) != 0)
347 		msg_fatal("%s: \"%s\"", err, req->raw_text);
348 #if 0
349 	    if ((mode & PCF_MASTER_PARAM)
350 	    && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)])
351 		msg_fatal("whitespace in parameter value: \"%s\"",
352 			  req->raw_text);
353 #endif
354 	} else if (mode & (PCF_COMMENT_OUT | PCF_EDIT_EXCL)) {
355 	    if (strchr(cp, '=') != 0)
356 		msg_fatal("-X or -# requires names without value");
357 	    pattern = cp;
358 	    trimblanks(pattern, 0);
359 	    req->edit_value = 0;
360 	} else {
361 	    msg_panic("%s: unknown mode %d", myname, mode);
362 	}
363 
364 #define PCF_MASTER_MASK (PCF_MASTER_ENTRY | PCF_MASTER_FLD | PCF_MASTER_PARAM)
365 
366 	/*
367 	 * Split name/type or name/type/whatever pattern into components.
368 	 */
369 	switch (mode & PCF_MASTER_MASK) {
370 	case PCF_MASTER_ENTRY:
371 	    if ((req->service_pattern =
372 		 pcf_parse_service_pattern(pattern, 2, 2)) == 0)
373 		msg_fatal("-Me, -MX or -M# requires service_name/type");
374 	    break;
375 	case PCF_MASTER_FLD:
376 	    if ((req->service_pattern =
377 		 pcf_parse_service_pattern(pattern, 3, 3)) == 0)
378 		msg_fatal("-Fe or -FX requires service_name/type/field_name");
379 	    req->field_number =
380 		pcf_parse_field_pattern(req->service_pattern->argv[2]);
381 	    if (pcf_is_magic_field_pattern(req->field_number))
382 		msg_fatal("-Fe does not accept wild-card field name");
383 	    if ((mode & PCF_EDIT_CONF)
384 		&& req->field_number < PCF_MASTER_FLD_CMD
385 	    && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)])
386 		msg_fatal("-Fe does not accept whitespace in non-command field");
387 	    break;
388 	case PCF_MASTER_PARAM:
389 	    if ((req->service_pattern =
390 		 pcf_parse_service_pattern(pattern, 3, 3)) == 0)
391 		msg_fatal("-Pe or -PX requires service_name/type/parameter");
392 	    req->param_pattern = req->service_pattern->argv[2];
393 	    if (PCF_IS_MAGIC_PARAM_PATTERN(req->param_pattern))
394 		msg_fatal("-Pe does not accept wild-card parameter name");
395 	    if ((mode & PCF_EDIT_CONF)
396 	    && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)])
397 		msg_fatal("-Pe does not accept whitespace in parameter value");
398 	    break;
399 	default:
400 	    msg_panic("%s: unknown edit mode %d", myname, mode);
401 	}
402     }
403 
404     /*
405      * Open a temp file for the result. This uses a deterministic name so we
406      * don't leave behind thrash with random names.
407      */
408     path = pcf_get_master_path();
409     if ((ep = edit_file_open(path, O_CREAT | O_WRONLY, 0644)) == 0)
410 	msg_fatal("open %s%s: %m", path, EDIT_FILE_SUFFIX);
411     dst = ep->tmp_fp;
412 
413     /*
414      * Open the original file for input.
415      */
416     if ((src = vstream_fopen(path, O_RDONLY, 0)) == 0) {
417 	/* OK to delete, since we control the temp file name exclusively. */
418 	(void) unlink(ep->tmp_path);
419 	msg_fatal("open %s for reading: %m", path);
420     }
421 
422     /*
423      * Copy original file to temp file, while replacing service entries on
424      * the fly.
425      */
426     service_name_type_matched = 0;
427     new_entry = 0;
428     lineno = 0;
429     while ((cp = pcf_next_cf_line(parse_buf, src, dst, &lineno)) != 0) {
430 	vstring_strcpy(line_buf, STR(parse_buf));
431 
432 	/*
433 	 * Copy, skip or replace continued text.
434 	 */
435 	if (cp > STR(parse_buf)) {
436 	    if (service_name_type_matched == 0)
437 		vstream_fputs(STR(line_buf), dst);
438 	    else if (mode & PCF_COMMENT_OUT)
439 		vstream_fprintf(dst, "#%s", STR(line_buf));
440 	}
441 
442 	/*
443 	 * Copy or replace (start of) logical line.
444 	 */
445 	else {
446 	    service_name_type_matched = 0;
447 
448 	    /*
449 	     * Parse out the service name and type.
450 	     */
451 	    if ((service_name = mystrtok(&cp, PCF_MASTER_BLANKS)) == 0
452 		|| (service_type = mystrtok(&cp, PCF_MASTER_BLANKS)) == 0)
453 		msg_fatal("file %s: line %d: specify service name and type "
454 			  "on the same line", path, lineno);
455 	    if (strchr(service_name, '='))
456 		msg_fatal("file %s: line %d: service name syntax \"%s\" is "
457 			  "unsupported with %s", path, lineno, service_name,
458 			  edit_opts);
459 	    if (service_type[strcspn(service_type, "=/")] != 0)
460 		msg_fatal("file %s: line %d: "
461 			"service type syntax \"%s\" is unsupported with %s",
462 			  path, lineno, service_type, edit_opts);
463 
464 	    /*
465 	     * Match each service pattern.
466 	     *
467 	     * Additional care is needed when a request adds or replaces an
468 	     * entire service definition, instead of a specific field or
469 	     * parameter. Given a command "postconf -M name1/type1='name2
470 	     * type2 ...'", where name1 and name2 may differ, and likewise
471 	     * for type1 and type2:
472 	     *
473 	     * - First, if an existing service definition a) matches the service
474 	     * pattern 'name1/type1', or b) matches the name and type in the
475 	     * new service definition 'name2 type2 ...', remove the service
476 	     * definition.
477 	     *
478 	     * - Then, after an a) or b) type match, add a new service
479 	     * definition for 'name2 type2 ...', but only after the first
480 	     * match.
481 	     *
482 	     * - Finally, if a request had no a) or b) type match for any
483 	     * master.cf service definition, add a new service definition for
484 	     * 'name2 type2 ...'.
485 	     */
486 	    for (req = edit_reqs; req < edit_reqs + num_reqs; req++) {
487 		PCF_MASTER_ENT *tentative_entry = 0;
488 		int     use_tentative_entry = 0;
489 
490 		/* Additional care for whole service definition requests. */
491 		if ((mode & PCF_MASTER_ENTRY) && (mode & PCF_EDIT_CONF)) {
492 		    tentative_entry = (PCF_MASTER_ENT *)
493 			mymalloc(sizeof(*tentative_entry));
494 		    if ((err = pcf_parse_master_entry(tentative_entry,
495 						      req->edit_value)) != 0)
496 			msg_fatal("%s: \"%s\"", err, req->raw_text);
497 		}
498 		if (PCF_MATCH_SERVICE_PATTERN(req->service_pattern,
499 					      service_name,
500 					      service_type)) {
501 		    service_name_type_matched = 1;	/* Sticky flag */
502 		    req->match_count += 1;
503 
504 		    /*
505 		     * Generate replacement master.cf entries.
506 		     */
507 		    if ((mode & PCF_EDIT_CONF)
508 			|| ((mode & PCF_MASTER_PARAM) && (mode & PCF_EDIT_EXCL))) {
509 			switch (mode & PCF_MASTER_MASK) {
510 
511 			    /*
512 			     * Replace master.cf entry field or parameter
513 			     * value.
514 			     */
515 			case PCF_MASTER_FLD:
516 			case PCF_MASTER_PARAM:
517 			    if (new_entry == 0) {
518 				/* Gobble up any continuation lines. */
519 				pcf_gobble_cf_line(full_entry_buf, line_buf,
520 						   src, dst, &lineno);
521 				new_entry = (PCF_MASTER_ENT *)
522 				    mymalloc(sizeof(*new_entry));
523 				if ((err = pcf_parse_master_entry(new_entry,
524 						 STR(full_entry_buf))) != 0)
525 				    msg_fatal("file %s: line %d: %s",
526 					      path, lineno, err);
527 			    }
528 			    if (mode & PCF_MASTER_FLD) {
529 				pcf_edit_master_field(new_entry,
530 						      req->field_number,
531 						      req->edit_value);
532 			    } else {
533 				pcf_edit_master_param(new_entry, mode,
534 						      req->param_pattern,
535 						      req->edit_value);
536 			    }
537 			    break;
538 
539 			    /*
540 			     * Replace entire master.cf entry.
541 			     */
542 			case PCF_MASTER_ENTRY:
543 			    if (req->match_count == 1)
544 				use_tentative_entry = 1;
545 			    break;
546 			default:
547 			    msg_panic("%s: unknown edit mode %d", myname, mode);
548 			}
549 		    }
550 		} else if (tentative_entry != 0
551 			 && PCF_MATCH_SERVICE_PATTERN(tentative_entry->argv,
552 						      service_name,
553 						      service_type)) {
554 		    service_name_type_matched = 1;	/* Sticky flag */
555 		    req->match_count += 1;
556 		    if (req->match_count == 1)
557 			use_tentative_entry = 1;
558 		}
559 		if (tentative_entry != 0) {
560 		    if (use_tentative_entry) {
561 			if (new_entry != 0)
562 			    pcf_free_master_entry(new_entry);
563 			new_entry = tentative_entry;
564 		    } else {
565 			pcf_free_master_entry(tentative_entry);
566 		    }
567 		}
568 	    }
569 
570 	    /*
571 	     * Pass through or replace the current input line.
572 	     */
573 	    if (new_entry) {
574 		pcf_print_master_entry(dst, PCF_FOLD_LINE, new_entry);
575 		pcf_free_master_entry(new_entry);
576 		new_entry = 0;
577 	    } else if (service_name_type_matched == 0) {
578 		vstream_fputs(STR(line_buf), dst);
579 	    } else if (mode & PCF_COMMENT_OUT) {
580 		vstream_fprintf(dst, "#%s", STR(line_buf));
581 	    }
582 	}
583     }
584 
585     /*
586      * Postprocessing: when editing entire service entries, generate new
587      * entries for services not found. Otherwise (editing fields or
588      * parameters), "service not found" is a fatal error.
589      */
590     for (req = edit_reqs; req < edit_reqs + num_reqs; req++) {
591 	if (req->match_count == 0) {
592 	    if ((mode & PCF_MASTER_ENTRY) && (mode & PCF_EDIT_CONF)) {
593 		new_entry = (PCF_MASTER_ENT *) mymalloc(sizeof(*new_entry));
594 		if ((err = pcf_parse_master_entry(new_entry, req->edit_value)) != 0)
595 		    msg_fatal("%s: \"%s\"", err, req->raw_text);
596 		pcf_print_master_entry(dst, PCF_FOLD_LINE, new_entry);
597 		pcf_free_master_entry(new_entry);
598 	    } else if ((mode & PCF_MASTER_ENTRY) == 0) {
599 		msg_warn("unmatched service_name/type: \"%s\"", req->raw_text);
600 	    }
601 	}
602     }
603 
604     /*
605      * When all is well, rename the temp file to the original one.
606      */
607     if (vstream_fclose(src))
608 	msg_fatal("read %s: %m", path);
609     if (edit_file_close(ep) != 0)
610 	msg_fatal("close %s%s: %m", path, EDIT_FILE_SUFFIX);
611 
612     /*
613      * Cleanup.
614      */
615     vstring_free(line_buf);
616     vstring_free(parse_buf);
617     vstring_free(full_entry_buf);
618     for (req = edit_reqs; req < edit_reqs + num_reqs; req++) {
619 	argv_free(req->service_pattern);
620 	myfree(req->parsed_text);
621     }
622     myfree((void *) edit_reqs);
623 }
624