I ran into a few minor nuisances with sed's -i mode, which are mostly 
compatible with gnu sed, but I reckon we should address.

The problem is sed works by writing the output to a second file in the
same directory as the original and after completion renaming the file
to the original. This has two disadvantages:
1) The inode number changes, resulting in loss of carefully crafted
   hardlinks.
2) We require to have write permission in the same directory as the
   original file, even if we don't want to have a backup file.

Diff below tries to solve this by doing the following.
Copy the file to the backup location (/tmp/sedXXXXXXXXXX if no
extension) and use the backup as the infile and the original as the
outfile.

Furthermore I changed the lstat to fstat, so we can edit symlinks (gsed
supports symlinks by replacing the symlink by a new real file, which is
also fun), and I extended the warning messages in process to show the
backup file if we crash during operation, so people will always know
where to recover the file in case of disaster.

Because process also error(FATAL, ...)s during process and we always
have a backup file I don't think the warning in sed.1 is worth keeping.

The only downside to this new approach (that I can think of) is that we
now temporarily have a file that is in an inconsistent state, but that's
not much different to writing a file with any other editor.

$ echo test > /usr/obj/test && dd if=/dev/zero of=/usr/obj/bloat
/usr/obj: write failed, file system is full
dd: /usr/obj/bloat: No space left on device
614113+0 records in
614112+0 records out
314425344 bytes transferred in 2.206 secs (142470196 bytes/sec)
$ ./obj/sed -i -f /tmp/test.sed /usr/obj/test                                   
                                   

/usr/obj: write failed, file system is full
sed: /usr/obj/test: No space left on device (backup at /tmp/sedgRPSLImG9N)
$ cat /tmp/test.sed
s/test/aaa<repeat to infinity>/
$ cat /tmp/sedgRPSLImG9N 
test
$ ls -i /tmp/test
104 /tmp/test
$ sed -i s/test/foo/ /tmp/test
$ ls -i /tmp/test             
104 /tmp/test
$ doas touch /etc/test
$ doas chown martijn /etc/test
$ echo test > /etc/test
$ sed -i s/test/foo/ /etc/test
$ cat /etc/test 
foo

The diff does change quite a few mechanics, so some scrutiny is welcome.

martijn@

Index: main.c
===================================================================
RCS file: /cvs/src/usr.bin/sed/main.c,v
retrieving revision 1.39
diff -u -p -r1.39 main.c
--- main.c      6 Dec 2018 20:16:04 -0000       1.39
+++ main.c      7 Dec 2018 07:31:54 -0000
@@ -35,6 +35,7 @@
 
 #include <sys/types.h>
 #include <sys/ioctl.h>
+#include <sys/param.h>
 #include <sys/stat.h>
 
 #include <ctype.h>
@@ -94,13 +95,13 @@ static int rval;    /* Exit status */
  */
 const char *fname;             /* File name. */
 const char *outfname;          /* Output file name */
-static char oldfname[PATH_MAX];        /* Old file name (for in-place editing) 
*/
-static char tmpfname[PATH_MAX];        /* Temporary file name (for in-place 
editing) */
+char oldfname[PATH_MAX];       /* Old file name (for in-place editing) */
 char *inplace;                 /* Inplace edit file extension */
 u_long linenum;
 
 static void add_compunit(enum e_cut, char *);
 static void add_file(char *);
+int copy(FILE *, FILE *);
 static int next_files_have_lines(void);
 
 int termwidth;
@@ -310,26 +311,46 @@ again:
        return (NULL);
 }
 
+int
+copy(FILE *src, FILE *dst)
+{
+       unsigned char buf[MAXBSIZE];
+       size_t r, w, tw;
+
+       while(1) {
+               if ((r = fread(buf, sizeof(*buf), sizeof(buf), src)) == 0) {
+                       if (feof(src)) {
+                               if (fflush(dst) == EOF)
+                                       return 0;
+                               return 1;
+                       }
+                       if (errno != EINTR)
+                               return 0;
+                       continue;
+               }
+               tw = 0;
+               while(tw < r) {
+                       w = fwrite(buf + tw, sizeof(*buf), r - tw, dst);
+                       if (w == 0) {
+                               if (errno != EINTR)
+                                       return 0;
+                               continue;
+                       }
+                       tw += w;
+               }
+       }
+}
+
 void
 finish_file(void)
 {
        if (infile != NULL) {
                fclose(infile);
-               if (*oldfname != '\0') {
-                       if (rename(fname, oldfname) != 0) {
-                               warning("rename()");
-                               unlink(tmpfname);
-                               exit(1);
-                       }
+               if (inplace != NULL) {
+                       if (*oldfname == '\0')
+                               unlink(oldfname);
                        *oldfname = '\0';
                }
-               if (*tmpfname != '\0') {
-                       if (outfile != NULL && outfile != stdout)
-                               fclose(outfile);
-                       outfile = NULL;
-                       rename(tmpfname, fname);
-                       *tmpfname = '\0';
-               }
                outfname = NULL;
        }
 }
@@ -341,7 +362,7 @@ finish_file(void)
 int
 mf_fgets(SPACE *sp, enum e_spflag spflag)
 {
-       struct stat sb;
+       struct stat sb, isb;
        size_t len;
        char *p;
        int c, fd;
@@ -369,10 +390,18 @@ mf_fgets(SPACE *sp, enum e_spflag spflag
                        outfname = "stdout";
                        break;
                }
+               infile = fopen(fname, inplace == NULL ? "r" : "r+");
+               if (infile == NULL) {
+                       warning("%s", strerror(errno));
+                       rval = 1;
+                       continue;
+               }
                if (inplace != NULL) {
-                       if (lstat(fname, &sb) != 0)
+                       /* inplace reads from copy */
+                       outfile = infile;
+                       if (fstat(fileno(outfile), &sb) != 0)
                                error(FATAL, "%s: %s", fname,
-                                   strerror(errno ? errno : EIO));
+                                   strerror(errno));
                        if (!S_ISREG(sb.st_mode))
                                error(FATAL, "%s: %s %s", fname,
                                    "in-place editing only",
@@ -382,32 +411,48 @@ mf_fgets(SPACE *sp, enum e_spflag spflag
                                    sizeof(oldfname));
                                len = strlcat(oldfname, inplace,
                                    sizeof(oldfname));
-                               if (len > sizeof(oldfname))
-                                       error(FATAL, "%s: name too long", 
fname);
-                       }
-                       len = snprintf(tmpfname, sizeof(tmpfname), 
"%s/sedXXXXXXXXXX",
-                           dirname(fname));
-                       if (len >= sizeof(tmpfname))
-                               error(FATAL, "%s: name too long", fname);
-                       if ((fd = mkstemp(tmpfname)) == -1)
-                               error(FATAL, "%s: %s", fname, strerror(errno));
-                       if ((outfile = fdopen(fd, "w")) == NULL) {
-                               unlink(tmpfname);
-                               error(FATAL, "%s", fname);
+                               if (len >= sizeof(oldfname))
+                                       error(FATAL, "backup %s: name too long",
+                                           fname);
+                               (void)unlink(oldfname);
+                               if ((infile = fopen(oldfname, "w+")) == NULL)
+                                       error(FATAL, "can't create backup "
+                                           "file: %s", oldfname);
+                               if (fstat(fileno(infile), &isb) != 0)
+                                       error(FATAL, "%s: %s", oldfname,
+                                           strerror(errno));
+                               if (!S_ISREG(isb.st_mode))
+                                       error(FATAL, "backup file %s not a "
+                                           "regular file and can't be "
+                                           "replaced", oldfname);
+                               fchown(fileno(infile), sb.st_uid, sb.st_gid);
+                               fchmod(fileno(infile), sb.st_mode & ALLPERMS);
+                       } else {
+                               (void)strlcpy(oldfname, "/tmp/sedXXXXXXXXXX",
+                                   sizeof(oldfname));
+                               if ((fd = mkstemp(oldfname)) == -1)
+                                       error(FATAL, "%s: %s", fname,
+                                                   strerror(errno));
+                               if ((infile = fdopen(fd, "a+")) == NULL) {
+                                       unlink(oldfname);
+                                       error(FATAL, "%s", fname);
+                               }
                        }
-                       fchown(fileno(outfile), sb.st_uid, sb.st_gid);
-                       fchmod(fileno(outfile), sb.st_mode & ALLPERMS);
-                       outfname = tmpfname;
+                       outfname = fname;
                        linenum = 0;
                        resetstate();
+                       if (!copy(outfile, infile)) {
+                               unlink(oldfname);
+                               error(FATAL, "%s: failed to create backup copy "
+                                   "to %s: %s", fname, oldfname,
+                                   strerror(errno));
+                       }
+                       ftruncate(fileno(outfile), 0);
+                       rewind(outfile);
+                       rewind(infile);
                } else {
                        outfile = stdout;
                        outfname = "stdout";
-               }
-               if ((infile = fopen(fname, "r")) == NULL) {
-                       warning("%s", strerror(errno));
-                       rval = 1;
-                       continue;
                }
        }
 
Index: process.c
===================================================================
RCS file: /cvs/src/usr.bin/sed/process.c,v
retrieving revision 1.34
diff -u -p -r1.34 process.c
--- process.c   14 Nov 2018 10:59:33 -0000      1.34
+++ process.c   7 Dec 2018 07:31:54 -0000
@@ -75,6 +75,7 @@ static int sdone;             /* If any substitutes
                                /* Iov structure for 'w' commands. */
 static regex_t *defpreg;
 size_t maxnsub;
+extern char oldfname[PATH_MAX];
 regmatch_t *match;
 
 #define OUT() do {\
@@ -473,7 +474,11 @@ flush_appends(void)
                        break;
                }
        if (ferror(outfile))
-               error(FATAL, "%s: %s", outfname, strerror(errno ? errno : EIO));
+               error(FATAL, "%s: %s%s%s%s", outfname,
+                   strerror(errno ? errno : EIO),
+                   *oldfname == '\0' ? "" : " (backup at ",
+                   *oldfname == '\0' ? "" : oldfname,
+                   *oldfname == '\0' ? "" : ")");
        appendx = sdone = 0;
 }
 
@@ -513,7 +518,11 @@ lputs(char *s)
        (void)fputc('$', outfile);
        (void)fputc('\n', outfile);
        if (ferror(outfile))
-               error(FATAL, "%s: %s", outfname, strerror(errno ? errno : EIO));
+               error(FATAL, "%s: %s%s%s%s", outfname,
+                   strerror(errno ? errno : EIO),
+                   *oldfname == '\0' ? "" : " (backup at ",
+                   *oldfname == '\0' ? "" : oldfname,
+                   *oldfname == '\0' ? "" : ")");
 }
 
 static inline int
Index: sed.1
===================================================================
RCS file: /cvs/src/usr.bin/sed/sed.1,v
retrieving revision 1.57
diff -u -p -r1.57 sed.1
--- sed.1       14 Nov 2018 10:59:33 -0000      1.57
+++ sed.1       7 Dec 2018 07:31:54 -0000
@@ -102,10 +102,6 @@ Edit files in place, saving backups with
 If a zero length
 .Ar extension
 is given, no backup will be saved.
-It is not recommended to give a zero length
-.Ar extension
-when in place editing files, as it risks corruption or partial content
-in situations where disk space is exhausted, etc.
 In
 .Fl i
 mode, the hold space, line numbers, and ranges are reset between files.

Reply via email to