summaryrefslogtreecommitdiffstats
path: root/src/postconf/postconf_edit.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 12:06:34 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 12:06:34 +0000
commit5e61585d76ae77fd5e9e96ebabb57afa4d74880d (patch)
tree2b467823aaeebc7ef8bc9e3cabe8074eaef1666d /src/postconf/postconf_edit.c
parentInitial commit. (diff)
downloadpostfix-5e61585d76ae77fd5e9e96ebabb57afa4d74880d.tar.xz
postfix-5e61585d76ae77fd5e9e96ebabb57afa4d74880d.zip
Adding upstream version 3.5.24.upstream/3.5.24upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/postconf/postconf_edit.c')
-rw-r--r--src/postconf/postconf_edit.c625
1 files changed, 625 insertions, 0 deletions
diff --git a/src/postconf/postconf_edit.c b/src/postconf/postconf_edit.c
new file mode 100644
index 0000000..2f9a607
--- /dev/null
+++ b/src/postconf/postconf_edit.c
@@ -0,0 +1,625 @@
+/*++
+/* NAME
+/* postconf_edit 3
+/* SUMMARY
+/* edit main.cf or master.cf
+/* SYNOPSIS
+/* #include <postconf.h>
+/*
+/* void pcf_edit_main(mode, argc, argv)
+/* int mode;
+/* int argc;
+/* char **argv;
+/*
+/* void pcf_edit_master(mode, argc, argv)
+/* int mode;
+/* int argc;
+/* char **argv;
+/* DESCRIPTION
+/* pcf_edit_main() edits the \fBmain.cf\fR configuration file.
+/* It replaces or adds parameter settings given as "\fIname=value\fR"
+/* pairs given on the command line, or removes parameter
+/* settings given as "\fIname\fR" on the command line. The
+/* file is copied to a temporary file then renamed into place.
+/*
+/* pcf_edit_master() edits the \fBmaster.cf\fR configuration
+/* file. The file is copied to a temporary file then renamed
+/* into place. Depending on the flags in \fBmode\fR:
+/* .IP PCF_MASTER_ENTRY
+/* With PCF_EDIT_CONF, pcf_edit_master() replaces or adds
+/* entire master.cf entries, specified on the command line as
+/* "\fIname/type = name type private unprivileged chroot wakeup
+/* process_limit command...\fR".
+/*
+/* With PCF_EDIT_EXCL or PCF_COMMENT_OUT, pcf_edit_master()
+/* removes or comments out entries specified on the command
+/* line as "\fIname/type\fR.
+/* .IP PCF_MASTER_FLD
+/* With PCF_EDIT_CONF, pcf_edit_master() replaces the value
+/* of specific service attributes, specified on the command
+/* line as "\fIname/type/attribute = value\fR".
+/* .IP PCF_MASTER_PARAM
+/* With PCF_EDIT_CONF, pcf_edit_master() replaces or adds the
+/* value of service parameters, specified on the command line
+/* as "\fIname/type/parameter = value\fR".
+/*
+/* With PCF_EDIT_EXCL, pcf_edit_master() removes service
+/* parameters specified on the command line as "\fIparametername\fR".
+/* DIAGNOSTICS
+/* Problems are reported to the standard error stream.
+/* FILES
+/* /etc/postfix/main.cf, Postfix configuration parameters
+/* /etc/postfix/main.cf.tmp, temporary name
+/* /etc/postfix/master.cf, Postfix configuration parameters
+/* /etc/postfix/master.cf.tmp, temporary name
+/* LICENSE
+/* .ad
+/* .fi
+/* The Secure Mailer license must be distributed with this software.
+/* AUTHOR(S)
+/* Wietse Venema
+/* IBM T.J. Watson Research
+/* P.O. Box 704
+/* Yorktown Heights, NY 10598, USA
+/*--*/
+
+/* System library. */
+
+#include <sys_defs.h>
+#include <string.h>
+#include <ctype.h>
+
+/* Utility library. */
+
+#include <msg.h>
+#include <mymalloc.h>
+#include <htable.h>
+#include <vstring.h>
+#include <vstring_vstream.h>
+#include <edit_file.h>
+#include <readlline.h>
+#include <stringops.h>
+#include <split_at.h>
+
+/* Global library. */
+
+#include <mail_params.h>
+
+/* Application-specific. */
+
+#include <postconf.h>
+
+#define STR(x) vstring_str(x)
+
+/* pcf_find_cf_info - pass-through non-content line, return content or null */
+
+static char *pcf_find_cf_info(VSTRING *buf, VSTREAM *dst)
+{
+ char *cp;
+
+ for (cp = STR(buf); ISSPACE(*cp) /* including newline */ ; cp++)
+ /* void */ ;
+ /* Pass-through comment, all-whitespace, or empty line. */
+ if (*cp == '#' || *cp == 0) {
+ vstream_fputs(STR(buf), dst);
+ return (0);
+ } else {
+ return (cp);
+ }
+}
+
+/* pcf_next_cf_line - return next content line, pass non-content */
+
+static char *pcf_next_cf_line(VSTRING *buf, VSTREAM *src, VSTREAM *dst, int *lineno)
+{
+ char *cp;
+
+ while (vstring_get(buf, src) != VSTREAM_EOF) {
+ if (lineno)
+ *lineno += 1;
+ if ((cp = pcf_find_cf_info(buf, dst)) != 0)
+ return (cp);
+ }
+ return (0);
+}
+
+/* pcf_gobble_cf_line - accumulate multi-line content, pass non-content */
+
+static void pcf_gobble_cf_line(VSTRING *full_entry_buf, VSTRING *line_buf,
+ VSTREAM *src, VSTREAM *dst, int *lineno)
+{
+ int ch;
+
+ vstring_strcpy(full_entry_buf, STR(line_buf));
+ for (;;) {
+ if ((ch = VSTREAM_GETC(src)) != VSTREAM_EOF)
+ vstream_ungetc(src, ch);
+ if ((ch != '#' && !ISSPACE(ch))
+ || vstring_get(line_buf, src) == VSTREAM_EOF)
+ break;
+ lineno += 1;
+ if (pcf_find_cf_info(line_buf, dst))
+ vstring_strcat(full_entry_buf, STR(line_buf));
+ }
+}
+
+/* pcf_edit_main - edit main.cf file */
+
+void pcf_edit_main(int mode, int argc, char **argv)
+{
+ char *path;
+ EDIT_FILE *ep;
+ VSTREAM *src;
+ VSTREAM *dst;
+ VSTRING *buf = vstring_alloc(100);
+ VSTRING *key = vstring_alloc(10);
+ char *cp;
+ char *pattern;
+ char *edit_value;
+ HTABLE *table;
+ struct cvalue {
+ char *value;
+ int found;
+ };
+ struct cvalue *cvalue;
+ HTABLE_INFO **ht_info;
+ HTABLE_INFO **ht;
+ int interesting;
+ const char *err;
+
+ /*
+ * Store command-line parameters for quick lookup.
+ */
+ table = htable_create(argc);
+ while ((cp = *argv++) != 0) {
+ if (strchr(cp, '\n') != 0)
+ msg_fatal("-e, -X, or -# accepts no multi-line input");
+ while (ISSPACE(*cp))
+ cp++;
+ if (*cp == '#')
+ msg_fatal("-e, -X, or -# accepts no comment input");
+ if (mode & PCF_EDIT_CONF) {
+ if ((err = split_nameval(cp, &pattern, &edit_value)) != 0)
+ msg_fatal("%s: \"%s\"", err, cp);
+ } else if (mode & (PCF_COMMENT_OUT | PCF_EDIT_EXCL)) {
+ if (*cp == 0)
+ msg_fatal("-X or -# requires non-blank parameter names");
+ if (strchr(cp, '=') != 0)
+ msg_fatal("-X or -# requires parameter names without value");
+ pattern = cp;
+ trimblanks(pattern, 0);
+ edit_value = 0;
+ } else {
+ msg_panic("pcf_edit_main: unknown mode %d", mode);
+ }
+ if ((cvalue = htable_find(table, pattern)) != 0) {
+ msg_warn("ignoring earlier request: '%s = %s'",
+ pattern, cvalue->value);
+ htable_delete(table, pattern, myfree);
+ }
+ cvalue = (struct cvalue *) mymalloc(sizeof(*cvalue));
+ cvalue->value = edit_value;
+ cvalue->found = 0;
+ htable_enter(table, pattern, (void *) cvalue);
+ }
+
+ /*
+ * Open a temp file for the result. This uses a deterministic name so we
+ * don't leave behind thrash with random names.
+ */
+ pcf_set_config_dir();
+ path = concatenate(var_config_dir, "/", MAIN_CONF_FILE, (char *) 0);
+ if ((ep = edit_file_open(path, O_CREAT | O_WRONLY, 0644)) == 0)
+ msg_fatal("open %s%s: %m", path, EDIT_FILE_SUFFIX);
+ dst = ep->tmp_fp;
+
+ /*
+ * Open the original file for input.
+ */
+ if ((src = vstream_fopen(path, O_RDONLY, 0)) == 0) {
+ /* OK to delete, since we control the temp file name exclusively. */
+ (void) unlink(ep->tmp_path);
+ msg_fatal("open %s for reading: %m", path);
+ }
+
+ /*
+ * Copy original file to temp file, while replacing parameters on the
+ * fly. Issue warnings for names found multiple times.
+ */
+#define STR(x) vstring_str(x)
+
+ interesting = 0;
+ while ((cp = pcf_next_cf_line(buf, src, dst, (int *) 0)) != 0) {
+ /* Copy, skip or replace continued text. */
+ if (cp > STR(buf)) {
+ if (interesting == 0)
+ vstream_fputs(STR(buf), dst);
+ else if (mode & PCF_COMMENT_OUT)
+ vstream_fprintf(dst, "#%s", STR(buf));
+ }
+ /* Copy or replace start of logical line. */
+ else {
+ vstring_strncpy(key, cp, strcspn(cp, CHARS_SPACE "="));
+ cvalue = (struct cvalue *) htable_find(table, STR(key));
+ if ((interesting = !!cvalue) != 0) {
+ if (cvalue->found++ == 1)
+ msg_warn("%s: multiple entries for \"%s\"", path, STR(key));
+ if (mode & PCF_EDIT_CONF)
+ vstream_fprintf(dst, "%s = %s\n", STR(key), cvalue->value);
+ else if (mode & PCF_COMMENT_OUT)
+ vstream_fprintf(dst, "#%s", cp);
+ } else {
+ vstream_fputs(STR(buf), dst);
+ }
+ }
+ }
+
+ /*
+ * Generate new entries for parameters that were not found.
+ */
+ if (mode & PCF_EDIT_CONF) {
+ for (ht_info = ht = htable_list(table); *ht; ht++) {
+ cvalue = (struct cvalue *) ht[0]->value;
+ if (cvalue->found == 0)
+ vstream_fprintf(dst, "%s = %s\n", ht[0]->key, cvalue->value);
+ }
+ myfree((void *) ht_info);
+ }
+
+ /*
+ * When all is well, rename the temp file to the original one.
+ */
+ if (vstream_fclose(src))
+ msg_fatal("read %s: %m", path);
+ if (edit_file_close(ep) != 0)
+ msg_fatal("close %s%s: %m", path, EDIT_FILE_SUFFIX);
+
+ /*
+ * Cleanup.
+ */
+ myfree(path);
+ vstring_free(buf);
+ vstring_free(key);
+ htable_free(table, myfree);
+}
+
+ /*
+ * Data structure to hold a master.cf edit request.
+ */
+typedef struct {
+ int match_count; /* hit count */
+ const char *raw_text; /* unparsed command-line argument */
+ char *parsed_text; /* destructive parse */
+ ARGV *service_pattern; /* service name, type, ... */
+ int field_number; /* attribute field number */
+ const char *param_pattern; /* parameter name */
+ char *edit_value; /* value substring */
+} PCF_MASTER_EDIT_REQ;
+
+/* pcf_edit_master - edit master.cf file */
+
+void pcf_edit_master(int mode, int argc, char **argv)
+{
+ const char *myname = "pcf_edit_master";
+ char *path;
+ EDIT_FILE *ep;
+ VSTREAM *src;
+ VSTREAM *dst;
+ VSTRING *line_buf = vstring_alloc(100);
+ VSTRING *parse_buf = vstring_alloc(100);
+ int lineno;
+ PCF_MASTER_ENT *new_entry;
+ VSTRING *full_entry_buf = vstring_alloc(100);
+ char *cp;
+ char *pattern;
+ int service_name_type_matched;
+ const char *err;
+ PCF_MASTER_EDIT_REQ *edit_reqs;
+ PCF_MASTER_EDIT_REQ *req;
+ int num_reqs = argc;
+ const char *edit_opts = "-Me, -Fe, -Pe, -X, or -#";
+ char *service_name;
+ char *service_type;
+
+ /*
+ * Sanity check.
+ */
+ if (num_reqs <= 0)
+ msg_panic("%s: empty argument list", myname);
+
+ /*
+ * Preprocessing: split pattern=value, then split the pattern components.
+ */
+ edit_reqs = (PCF_MASTER_EDIT_REQ *) mymalloc(sizeof(*edit_reqs) * num_reqs);
+ for (req = edit_reqs; *argv != 0; req++, argv++) {
+ req->match_count = 0;
+ req->raw_text = *argv;
+ cp = req->parsed_text = mystrdup(req->raw_text);
+ if (strchr(cp, '\n') != 0)
+ msg_fatal("%s accept no multi-line input", edit_opts);
+ while (ISSPACE(*cp))
+ cp++;
+ if (*cp == '#')
+ msg_fatal("%s accept no comment input", edit_opts);
+ /* Separate the pattern from the value. */
+ if (mode & PCF_EDIT_CONF) {
+ if ((err = split_nameval(cp, &pattern, &req->edit_value)) != 0)
+ msg_fatal("%s: \"%s\"", err, req->raw_text);
+#if 0
+ if ((mode & PCF_MASTER_PARAM)
+ && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)])
+ msg_fatal("whitespace in parameter value: \"%s\"",
+ req->raw_text);
+#endif
+ } else if (mode & (PCF_COMMENT_OUT | PCF_EDIT_EXCL)) {
+ if (strchr(cp, '=') != 0)
+ msg_fatal("-X or -# requires names without value");
+ pattern = cp;
+ trimblanks(pattern, 0);
+ req->edit_value = 0;
+ } else {
+ msg_panic("%s: unknown mode %d", myname, mode);
+ }
+
+#define PCF_MASTER_MASK (PCF_MASTER_ENTRY | PCF_MASTER_FLD | PCF_MASTER_PARAM)
+
+ /*
+ * Split name/type or name/type/whatever pattern into components.
+ */
+ switch (mode & PCF_MASTER_MASK) {
+ case PCF_MASTER_ENTRY:
+ if ((req->service_pattern =
+ pcf_parse_service_pattern(pattern, 2, 2)) == 0)
+ msg_fatal("-Me, -MX or -M# requires service_name/type");
+ break;
+ case PCF_MASTER_FLD:
+ if ((req->service_pattern =
+ pcf_parse_service_pattern(pattern, 3, 3)) == 0)
+ msg_fatal("-Fe or -FX requires service_name/type/field_name");
+ req->field_number =
+ pcf_parse_field_pattern(req->service_pattern->argv[2]);
+ if (pcf_is_magic_field_pattern(req->field_number))
+ msg_fatal("-Fe does not accept wild-card field name");
+ if ((mode & PCF_EDIT_CONF)
+ && req->field_number < PCF_MASTER_FLD_CMD
+ && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)])
+ msg_fatal("-Fe does not accept whitespace in non-command field");
+ break;
+ case PCF_MASTER_PARAM:
+ if ((req->service_pattern =
+ pcf_parse_service_pattern(pattern, 3, 3)) == 0)
+ msg_fatal("-Pe or -PX requires service_name/type/parameter");
+ req->param_pattern = req->service_pattern->argv[2];
+ if (PCF_IS_MAGIC_PARAM_PATTERN(req->param_pattern))
+ msg_fatal("-Pe does not accept wild-card parameter name");
+ if ((mode & PCF_EDIT_CONF)
+ && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)])
+ msg_fatal("-Pe does not accept whitespace in parameter value");
+ break;
+ default:
+ msg_panic("%s: unknown edit mode %d", myname, mode);
+ }
+ }
+
+ /*
+ * Open a temp file for the result. This uses a deterministic name so we
+ * don't leave behind thrash with random names.
+ */
+ pcf_set_config_dir();
+ path = concatenate(var_config_dir, "/", MASTER_CONF_FILE, (char *) 0);
+ if ((ep = edit_file_open(path, O_CREAT | O_WRONLY, 0644)) == 0)
+ msg_fatal("open %s%s: %m", path, EDIT_FILE_SUFFIX);
+ dst = ep->tmp_fp;
+
+ /*
+ * Open the original file for input.
+ */
+ if ((src = vstream_fopen(path, O_RDONLY, 0)) == 0) {
+ /* OK to delete, since we control the temp file name exclusively. */
+ (void) unlink(ep->tmp_path);
+ msg_fatal("open %s for reading: %m", path);
+ }
+
+ /*
+ * Copy original file to temp file, while replacing service entries on
+ * the fly.
+ */
+ service_name_type_matched = 0;
+ new_entry = 0;
+ lineno = 0;
+ while ((cp = pcf_next_cf_line(parse_buf, src, dst, &lineno)) != 0) {
+ vstring_strcpy(line_buf, STR(parse_buf));
+
+ /*
+ * Copy, skip or replace continued text.
+ */
+ if (cp > STR(parse_buf)) {
+ if (service_name_type_matched == 0)
+ vstream_fputs(STR(line_buf), dst);
+ else if (mode & PCF_COMMENT_OUT)
+ vstream_fprintf(dst, "#%s", STR(line_buf));
+ }
+
+ /*
+ * Copy or replace (start of) logical line.
+ */
+ else {
+ service_name_type_matched = 0;
+
+ /*
+ * Parse out the service name and type.
+ */
+ if ((service_name = mystrtok(&cp, PCF_MASTER_BLANKS)) == 0
+ || (service_type = mystrtok(&cp, PCF_MASTER_BLANKS)) == 0)
+ msg_fatal("file %s: line %d: specify service name and type "
+ "on the same line", path, lineno);
+ if (strchr(service_name, '='))
+ msg_fatal("file %s: line %d: service name syntax \"%s\" is "
+ "unsupported with %s", path, lineno, service_name,
+ edit_opts);
+ if (service_type[strcspn(service_type, "=/")] != 0)
+ msg_fatal("file %s: line %d: "
+ "service type syntax \"%s\" is unsupported with %s",
+ path, lineno, service_type, edit_opts);
+
+ /*
+ * Match each service pattern.
+ *
+ * Additional care is needed when a request adds or replaces an
+ * entire service definition, instead of a specific field or
+ * parameter. Given a command "postconf -M name1/type1='name2
+ * type2 ...'", where name1 and name2 may differ, and likewise
+ * for type1 and type2:
+ *
+ * - First, if an existing service definition a) matches the service
+ * pattern 'name1/type1', or b) matches the name and type in the
+ * new service definition 'name2 type2 ...', remove the service
+ * definition.
+ *
+ * - Then, after an a) or b) type match, add a new service
+ * definition for 'name2 type2 ...', but only after the first
+ * match.
+ *
+ * - Finally, if a request had no a) or b) type match for any
+ * master.cf service definition, add a new service definition for
+ * 'name2 type2 ...'.
+ */
+ for (req = edit_reqs; req < edit_reqs + num_reqs; req++) {
+ PCF_MASTER_ENT *tentative_entry = 0;
+ int use_tentative_entry = 0;
+
+ /* Additional care for whole service definition requests. */
+ if ((mode & PCF_MASTER_ENTRY) && (mode & PCF_EDIT_CONF)) {
+ tentative_entry = (PCF_MASTER_ENT *)
+ mymalloc(sizeof(*tentative_entry));
+ if ((err = pcf_parse_master_entry(tentative_entry,
+ req->edit_value)) != 0)
+ msg_fatal("%s: \"%s\"", err, req->raw_text);
+ }
+ if (PCF_MATCH_SERVICE_PATTERN(req->service_pattern,
+ service_name,
+ service_type)) {
+ service_name_type_matched = 1; /* Sticky flag */
+ req->match_count += 1;
+
+ /*
+ * Generate replacement master.cf entries.
+ */
+ if ((mode & PCF_EDIT_CONF)
+ || ((mode & PCF_MASTER_PARAM) && (mode & PCF_EDIT_EXCL))) {
+ switch (mode & PCF_MASTER_MASK) {
+
+ /*
+ * Replace master.cf entry field or parameter
+ * value.
+ */
+ case PCF_MASTER_FLD:
+ case PCF_MASTER_PARAM:
+ if (new_entry == 0) {
+ /* Gobble up any continuation lines. */
+ pcf_gobble_cf_line(full_entry_buf, line_buf,
+ src, dst, &lineno);
+ new_entry = (PCF_MASTER_ENT *)
+ mymalloc(sizeof(*new_entry));
+ if ((err = pcf_parse_master_entry(new_entry,
+ STR(full_entry_buf))) != 0)
+ msg_fatal("file %s: line %d: %s",
+ path, lineno, err);
+ }
+ if (mode & PCF_MASTER_FLD) {
+ pcf_edit_master_field(new_entry,
+ req->field_number,
+ req->edit_value);
+ } else {
+ pcf_edit_master_param(new_entry, mode,
+ req->param_pattern,
+ req->edit_value);
+ }
+ break;
+
+ /*
+ * Replace entire master.cf entry.
+ */
+ case PCF_MASTER_ENTRY:
+ if (req->match_count == 1)
+ use_tentative_entry = 1;
+ break;
+ default:
+ msg_panic("%s: unknown edit mode %d", myname, mode);
+ }
+ }
+ } else if (tentative_entry != 0
+ && PCF_MATCH_SERVICE_PATTERN(tentative_entry->argv,
+ service_name,
+ service_type)) {
+ service_name_type_matched = 1; /* Sticky flag */
+ req->match_count += 1;
+ if (req->match_count == 1)
+ use_tentative_entry = 1;
+ }
+ if (tentative_entry != 0) {
+ if (use_tentative_entry) {
+ if (new_entry != 0)
+ pcf_free_master_entry(new_entry);
+ new_entry = tentative_entry;
+ } else {
+ pcf_free_master_entry(tentative_entry);
+ }
+ }
+ }
+
+ /*
+ * Pass through or replace the current input line.
+ */
+ if (new_entry) {
+ pcf_print_master_entry(dst, PCF_FOLD_LINE, new_entry);
+ pcf_free_master_entry(new_entry);
+ new_entry = 0;
+ } else if (service_name_type_matched == 0) {
+ vstream_fputs(STR(line_buf), dst);
+ } else if (mode & PCF_COMMENT_OUT) {
+ vstream_fprintf(dst, "#%s", STR(line_buf));
+ }
+ }
+ }
+
+ /*
+ * Postprocessing: when editing entire service entries, generate new
+ * entries for services not found. Otherwise (editing fields or
+ * parameters), "service not found" is a fatal error.
+ */
+ for (req = edit_reqs; req < edit_reqs + num_reqs; req++) {
+ if (req->match_count == 0) {
+ if ((mode & PCF_MASTER_ENTRY) && (mode & PCF_EDIT_CONF)) {
+ new_entry = (PCF_MASTER_ENT *) mymalloc(sizeof(*new_entry));
+ if ((err = pcf_parse_master_entry(new_entry, req->edit_value)) != 0)
+ msg_fatal("%s: \"%s\"", err, req->raw_text);
+ pcf_print_master_entry(dst, PCF_FOLD_LINE, new_entry);
+ pcf_free_master_entry(new_entry);
+ } else if ((mode & PCF_MASTER_ENTRY) == 0) {
+ msg_warn("unmatched service_name/type: \"%s\"", req->raw_text);
+ }
+ }
+ }
+
+ /*
+ * When all is well, rename the temp file to the original one.
+ */
+ if (vstream_fclose(src))
+ msg_fatal("read %s: %m", path);
+ if (edit_file_close(ep) != 0)
+ msg_fatal("close %s%s: %m", path, EDIT_FILE_SUFFIX);
+
+ /*
+ * Cleanup.
+ */
+ myfree(path);
+ vstring_free(line_buf);
+ vstring_free(parse_buf);
+ vstring_free(full_entry_buf);
+ for (req = edit_reqs; req < edit_reqs + num_reqs; req++) {
+ argv_free(req->service_pattern);
+ myfree(req->parsed_text);
+ }
+ myfree((void *) edit_reqs);
+}