1 /*
2 * inplace.c - Provide support for in-place editing.
3 */
4
5 /*
6 * Copyright (C) 2013-2015, 2017, 2018, the Free Software Foundation, Inc.
7 *
8 * This file is part of GAWK, the GNU implementation of the
9 * AWK Programming Language.
10 *
11 * GAWK is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * GAWK is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License
22 * along with this program; if not, write to the Free Software
23 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 */
25
26 #ifdef HAVE_CONFIG_H
27 #include <config.h>
28 #endif
29
30 #ifndef _XOPEN_SOURCE
31 # define _XOPEN_SOURCE 1
32 #endif
33 #ifndef _XOPEN_SOURCE_EXTENDED
34 # define _XOPEN_SOURCE_EXTENDED 1
35 #endif
36
37 #include <stdio.h>
38 #include <assert.h>
39 #include <errno.h>
40 #include <fcntl.h>
41 #include <stdlib.h>
42 #include <string.h>
43 #include <unistd.h>
44
45 #include <sys/types.h>
46 #include <sys/stat.h>
47
48 #include "gawkapi.h"
49
50 #include "gettext.h"
51 #define _(msgid) gettext(msgid)
52 #define N_(msgid) msgid
53
54 #if ! defined(S_ISREG) && defined(S_IFREG)
55 #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
56 #endif
57
58 #ifdef __MINGW32__
59 # define chown(x,y,z) (0)
60 # define link(f1,f2) rename(f1,f2)
61 int
62 mkstemp (char *template)
63 {
64 char *tmp_fname = _mktemp (template);
65
66 if (tmp_fname)
67 return _open (tmp_fname, O_RDWR | O_CREAT | O_EXCL, S_IREAD | S_IWRITE);
68 return -1;
69 }
70 #endif
71
72 static const gawk_api_t *api; /* for convenience macros to work */
73 static awk_ext_id_t ext_id;
74 static const char *ext_version = "inplace extension: version 1.0";
75
76 int plugin_is_GPL_compatible;
77
78 static struct {
79 char *tname;
80 int default_stdout;
81 int posrc; /* return code from fgetpos */
82 fpos_t pos;
83 } state = { NULL, -1 };
84
85 /*
86 * XXX Known problems:
87 * 1. Should copy ACL.
88 * 2. Not reentrant, so will not work if multiple files are open at
89 * the same time. I'm not sure this is a meaningful problem in practice.
90 */
91
92 static void
93 at_exit(void *data, int exit_status)
94 {
95 (void) data; /* silence warnings */
96 (void) exit_status; /* silence warnings */
97 if (state.tname) {
98 unlink(state.tname);
99 gawk_free(state.tname);
100 state.tname = NULL;
101 }
102 }
103
104 /*
105 * N.B. Almost everything is a fatal error because this feature is typically
106 * used for one-liners where the user is not going to be worrying about
107 * checking errors. If anything unexpected occurs, we want to abort
108 * immediately!
109 */
110
111 static int
112 invalid_filename(const awk_string_t *filename)
113 {
114 return filename->len == 0 ||
115 (filename->len == 1 && *filename->str == '-');
116 }
117
118 /* do_inplace_begin --- start in-place editing */
119
120 static awk_value_t *
121 do_inplace_begin(int nargs, awk_value_t *result, struct awk_ext_func *unused)
122 {
123 awk_value_t filename;
124 struct stat sbuf;
125 int fd;
126
127 assert(result != NULL);
128 fflush(stdout);
129
130 if (state.tname)
131 fatal(ext_id, _("inplace::begin: in-place editing already active"));
132
133 if (nargs != 2)
134 fatal(ext_id, _("inplace::begin: expects 2 arguments but called with %d"), nargs);
135
136 if (! get_argument(0, AWK_STRING, &filename))
137 fatal(ext_id, _("inplace::begin: cannot retrieve 1st argument as a string filename"));
138
139 /*
140 * N.B. In the current implementation, the 2nd suffix arg is not used
141 * in this function. It is used only in the inplace_end function.
142 */
143
144 if (invalid_filename(&filename.str_value)) {
145 warning(ext_id, _("inplace::begin: disabling in-place editing for invalid FILENAME `%s'"),
146 filename.str_value.str);
147 unset_ERRNO();
148 return make_number(-1, result);
149 }
150
151 if (stat(filename.str_value.str, & sbuf) < 0) {
152 warning(ext_id, _("inplace::begin: Cannot stat `%s' (%s)"),
153 filename.str_value.str, strerror(errno));
154 update_ERRNO_int(errno);
155 return make_number(-1, result);
156 }
157
158 if (! S_ISREG(sbuf.st_mode)) {
159 warning(ext_id, _("inplace::begin: `%s' is not a regular file"),
160 filename.str_value.str);
161 unset_ERRNO();
162 return make_number(-1, result);
163 }
164
165 /* create a temporary file to which to redirect stdout */
166 emalloc(state.tname, char *, filename.str_value.len+14, "do_inplace_begin");
167 sprintf(state.tname, "%s.gawk.XXXXXX", filename.str_value.str);
168
169 if ((fd = mkstemp(state.tname)) < 0)
170 fatal(ext_id, _("inplace::begin: mkstemp(`%s') failed (%s)"),
171 state.tname, strerror(errno));
172
173 /* N.B. chown/chmod should be more portable than fchown/fchmod */
174 if (chown(state.tname, sbuf.st_uid, sbuf.st_gid) < 0) {
175 /* jumping through hoops to silence gcc and clang. :-( */
176 int junk;
177 junk = chown(state.tname, -1, sbuf.st_gid);
178 ++junk;
179 }
180
181 if (chmod(state.tname, sbuf.st_mode) < 0)
182 fatal(ext_id, _("inplace::begin: chmod failed (%s)"),
183 strerror(errno));
184
185 fflush(stdout);
186 /* N.B. fgetpos fails when stdout is a tty */
187 state.posrc = fgetpos(stdout, &state.pos);
188 if ((state.default_stdout = dup(STDOUT_FILENO)) < 0)
189 fatal(ext_id, _("inplace::begin: dup(stdout) failed (%s)"),
190 strerror(errno));
191 if (dup2(fd, STDOUT_FILENO) < 0)
192 fatal(ext_id, _("inplace::begin: dup2(%d, stdout) failed (%s)"),
193 fd, strerror(errno));
194 if (close(fd) < 0)
195 fatal(ext_id, _("inplace::begin: close(%d) failed (%s)"),
196 fd, strerror(errno));
197 rewind(stdout);
198 return make_number(0, result);
199 }
200
201 /* do_inplace_end --- finish in-place editing */
202
203 static awk_value_t *
204 do_inplace_end(int nargs, awk_value_t *result, struct awk_ext_func *unused)
205 {
206 awk_value_t filename, suffix;
207
208 assert(result != NULL);
209
210 if (nargs != 2)
211 fatal(ext_id, _("inplace::end: expects 2 arguments but called with %d"), nargs);
212
213 if (! get_argument(0, AWK_STRING, &filename))
214 fatal(ext_id, _("inplace::end: cannot retrieve 1st argument as a string filename"));
215
216 if (! get_argument(1, AWK_STRING, &suffix))
217 suffix.str_value.str = NULL;
218
219 if (! state.tname) {
220 if (! invalid_filename(&filename.str_value))
221 warning(ext_id, _("inplace::end: in-place editing not active"));
222 return make_number(0, result);
223 }
224
225 fflush(stdout);
226 if (dup2(state.default_stdout, STDOUT_FILENO) < 0)
227 fatal(ext_id, _("inplace::end: dup2(%d, stdout) failed (%s)"),
228 state.default_stdout, strerror(errno));
229 if (close(state.default_stdout) < 0)
230 fatal(ext_id, _("inplace::end: close(%d) failed (%s)"),
231 state.default_stdout, strerror(errno));
232 state.default_stdout = -1;
233 if (state.posrc == 0 && fsetpos(stdout, &state.pos) < 0)
234 fatal(ext_id, _("inplace::end: fsetpos(stdout) failed (%s)"),
235 strerror(errno));
236
237 if (suffix.str_value.str && suffix.str_value.str[0]) {
238 /* backup requested */
239 char *bakname;
240
241 emalloc(bakname, char *, filename.str_value.len+suffix.str_value.len+1,
242 "do_inplace_end");
243 sprintf(bakname, "%s%s",
244 filename.str_value.str, suffix.str_value.str);
245 unlink(bakname); /* if backup file exists already, remove it */
246 if (link(filename.str_value.str, bakname) < 0)
247 fatal(ext_id, _("inplace::end: link(`%s', `%s') failed (%s)"),
248 filename.str_value.str, bakname, strerror(errno));
249 gawk_free(bakname);
250 }
251
252 #ifdef __MINGW32__
253 unlink(filename.str_value.str);
254 #endif
255
256 if (rename(state.tname, filename.str_value.str) < 0)
257 fatal(ext_id, _("inplace::end: rename(`%s', `%s') failed (%s)"),
258 state.tname, filename.str_value.str, strerror(errno));
259 gawk_free(state.tname);
260 state.tname = NULL;
261 return make_number(0, result);
262 }
263
264 static awk_ext_func_t func_table[] = {
265 { "begin", do_inplace_begin, 2, 2, awk_false, NULL },
266 { "end", do_inplace_end, 2, 2, awk_false, NULL },
267 };
268
269 static awk_bool_t init_inplace(void)
270 {
271 awk_atexit(at_exit, NULL);
272 return awk_true;
273 }
274
275 static awk_bool_t (*init_func)(void) = init_inplace;
276
277 /* define the dl_load function using the boilerplate macro */
278
279 dl_load_func(func_table, inplace, "inplace")