From 100479bfdac007cb9d8a59ffcebfe903024f9578 Mon Sep 17 00:00:00 2001
From: Niels Thykier <niels@thykier.net>
Date: Wed, 22 Apr 2020 15:38:46 +0000
Subject: [PATCH] Support rm_conffile natively in dpkg via DEBIAN/conffiles

This commit adds a new "remove-on-upgrade" flag in DEBIAN/conffiles to
tell dpkg that the package wants to remove a given conffile (without
having to rely on maintscripts).

The flag is added to the conffile line after a colon and therefore
restricts the use of colon in conffile names. However, given it is the
Linux path separator character it is rarely used in filenames to begin
with.

Signed-off-by: Niels Thykier <niels@thykier.net>
---
 dpkg-deb/build.c   | 33 ++++++++++++++++++++++++++++++---
 lib/dpkg/dpkg-db.h |  1 +
 lib/dpkg/dump.c    |  2 ++
 lib/dpkg/fields.c  | 11 ++++++++++-
 lib/dpkg/fsys.h    |  2 ++
 src/archives.c     |  2 +-
 src/configure.c    | 36 +++++++++++++++++++++++++++++++++++-
 src/help.c         |  2 +-
 src/unpack.c       | 39 +++++++++++++++++++++++++++++++++++----
 9 files changed, 117 insertions(+), 11 deletions(-)

diff --git a/dpkg-deb/build.c b/dpkg-deb/build.c
index 3317b517a6..323256cacf 100644
--- a/dpkg-deb/build.c
+++ b/dpkg-deb/build.c
@@ -268,6 +268,8 @@ check_conffiles(const char *ctrldir, const char *rootdir)
   while (fgets(conffilename, MAXCONFFILENAME + 1, cf)) {
     struct stat controlstab;
     int n;
+    char *flags_pos;
+    bool remove_on_upgrade = false;
 
     n = strlen(conffilename);
     if (!n)
@@ -278,6 +280,27 @@ check_conffiles(const char *ctrldir, const char *rootdir)
              conffilename);
 
     conffilename[n - 1] = '\0';
+    flags_pos = strstr(conffilename, ":");
+    if (flags_pos) {
+      int flag_len;
+      *flags_pos = '\0';
+      n = strlen(conffilename);
+      ++flags_pos;
+      flag_len = strlen(flags_pos);
+      if (strcmp(flags_pos, "remove-on-upgrade") == 0) {
+        remove_on_upgrade = true;
+      } else {
+        if ((flag_len > 1) && c_isspace(flags_pos[flag_len - 2]))
+            warning(_("line with conffile filename '%s' contains trailing "
+                      "white spaces"), conffilename);
+        if ((flag_len > 1) && c_isspace(flags_pos[0]))
+            warning(_("line with conffile filename '%s' contains leading "
+                      "white spaces before conffile flag"), conffilename);
+        ohshit(_("Unknown flag '%s' for conffile '%s'"), flags_pos,
+               conffilename);
+      }
+    }
+
     varbuf_reset(&controlfile);
     varbuf_printf(&controlfile, "%s/%s", rootdir, conffilename);
     if (lstat(controlfile.buf, &controlstab)) {
@@ -285,9 +308,13 @@ check_conffiles(const char *ctrldir, const char *rootdir)
         if ((n > 1) && c_isspace(conffilename[n - 2]))
           warning(_("conffile filename '%s' contains trailing white spaces"),
                   conffilename);
-        ohshit(_("conffile '%.250s' does not appear in package"), conffilename);
-      } else
-        ohshite(_("conffile '%.250s' is not stattable"), conffilename);
+        if (!remove_on_upgrade)
+          ohshit(_("conffile '%.250s' does not appear in package"),
+                 conffilename);
+      }
+    } else if (remove_on_upgrade) {
+        ohshite(_("conffile '%.250s' is present but is requested to be"
+                  " removed"), conffilename);
     } else if (!S_ISREG(controlstab.st_mode)) {
       warning(_("conffile '%s' is not a plain file"), conffilename);
     }
diff --git a/lib/dpkg/dpkg-db.h b/lib/dpkg/dpkg-db.h
index 1ed116bd4b..9073f9c79c 100644
--- a/lib/dpkg/dpkg-db.h
+++ b/lib/dpkg/dpkg-db.h
@@ -82,6 +82,7 @@ struct conffile {
   const char *name;
   const char *hash;
   bool obsolete;
+  bool remove_on_upgrade;
 };
 
 struct archivedetails {
diff --git a/lib/dpkg/dump.c b/lib/dpkg/dump.c
index 48f0beb03f..8fdcf5feba 100644
--- a/lib/dpkg/dump.c
+++ b/lib/dpkg/dump.c
@@ -394,6 +394,8 @@ w_conffiles(struct varbuf *vb,
     varbuf_add_str(vb, i->hash);
     if (i->obsolete)
       varbuf_add_str(vb, " obsolete");
+    if (i->remove_on_upgrade)
+      varbuf_add_str(vb, " remove-on-upgrade");
   }
   if (flags&fw_printheader)
     varbuf_add_char(vb, '\n');
diff --git a/lib/dpkg/fields.c b/lib/dpkg/fields.c
index 8294149b9e..66764fcfe3 100644
--- a/lib/dpkg/fields.c
+++ b/lib/dpkg/fields.c
@@ -346,10 +346,11 @@ f_conffiles(struct pkginfo *pkg, struct pkgbin *pkgbin,
             const char *value, const struct fieldinfo *fip)
 {
   static const char obsolete_str[]= "obsolete";
+  static const char remove_on_upgrade_str[]= "remove-on-upgrade";
   struct conffile **lastp, *newlink;
   const char *endent, *endfn, *hashstart;
   int c, namelen, hashlen;
-  bool obsolete;
+  bool obsolete, remove_on_upgrade;
   char *newptr;
 
   lastp = &pkgbin->conffiles;
@@ -364,6 +365,13 @@ f_conffiles(struct pkginfo *pkg, struct pkgbin *pkgbin,
     conffvalue_lastword(value, endent, endent,
 			&hashstart, &hashlen, &endfn,
                         ps);
+    remove_on_upgrade= (hashlen == sizeof(remove_on_upgrade_str)-1 &&
+                        memcmp(hashstart, remove_on_upgrade_str, hashlen) == 0);
+    if (remove_on_upgrade)
+      conffvalue_lastword(value, endfn, endent,
+			  &hashstart, &hashlen, &endfn,
+			  ps);
+
     obsolete= (hashlen == sizeof(obsolete_str)-1 &&
                memcmp(hashstart, obsolete_str, hashlen) == 0);
     if (obsolete)
@@ -387,6 +395,7 @@ f_conffiles(struct pkginfo *pkg, struct pkgbin *pkgbin,
     newptr[hashlen] = '\0';
     newlink->hash= newptr;
     newlink->obsolete= obsolete;
+    newlink->remove_on_upgrade = remove_on_upgrade;
     newlink->next =NULL;
     *lastp= newlink;
     lastp= &newlink->next;
diff --git a/lib/dpkg/fsys.h b/lib/dpkg/fsys.h
index 8b9107472c..93af901559 100644
--- a/lib/dpkg/fsys.h
+++ b/lib/dpkg/fsys.h
@@ -77,6 +77,8 @@ enum fsys_namenode_flags {
 	FNNF_DEFERRED_RENAME		= DPKG_BIT(8),
 	/** Path being filtered. */
 	FNNF_FILTERED			= DPKG_BIT(9),
+	/** Conffile removal requested by upgrade */
+	FNNF_RM_CONFF_ON_UPGRADE	= DPKG_BIT(10),
 };
 
 /**
diff --git a/src/archives.c b/src/archives.c
index b147281e86..8f68075fec 100644
--- a/src/archives.c
+++ b/src/archives.c
@@ -235,7 +235,7 @@ md5hash_prev_conffile(struct pkginfo *pkg, char *oldhash, const char *oldname,
                              &otherpkg->configversion) != 0)
       continue;
     for (conff = otherpkg->installed.conffiles; conff; conff = conff->next) {
-      if (conff->obsolete)
+      if (conff->obsolete || conff->remove_on_upgrade)
         continue;
       if (strcmp(conff->name, namenode->name) == 0)
         break;
diff --git a/src/configure.c b/src/configure.c
index 12f290451b..c0a298d0c3 100644
--- a/src/configure.c
+++ b/src/configure.c
@@ -356,7 +356,7 @@ deferred_configure_ghost_conffile(struct pkginfo *pkg, struct conffile *conff)
 
 		for (otherconff = otherpkg->installed.conffiles; otherconff;
 		     otherconff = otherconff->next) {
-			if (otherconff->obsolete)
+			if (otherconff->obsolete || otherconff->remove_on_upgrade)
 				continue;
 
 			/* Check if we need to propagate the new hash from
@@ -401,6 +401,40 @@ deferred_configure_conffile(struct pkginfo *pkg, struct conffile *conff)
 	cdr2rest = cdr2.buf + strlen(cdr.buf);
 	/* From now on we can just strcpy(cdr2rest, extension); */
 
+	if (conff->remove_on_upgrade) {
+		/* remove DPKGDISTEXT variant if still present */
+		strcpy(cdr2rest, DPKGOLDEXT);
+		if (unlink(cdr2.buf) && errno != ENOENT)
+			warning(_("%s: failed to remove '%.250s': %s"),
+				  pkg_name(pkg, pnaw_nonambig), cdr2.buf,
+				  strerror(errno));
+		if (strcmp(currenthash, NONEXISTENTFLAG) == 0) {
+			/* Already removed (e.g. by local admin) */
+			return;
+		}
+		if (strcmp(conff->hash, currenthash) != 0) {
+			/* rename */
+			strcpy(cdr2rest, DPKGOLDEXT);
+
+			printf(_("Obsolete conffile '%.250s' has been modified "
+			         "by you.\n"), cdr.buf);
+			printf(_("Saving as %.250s ...\n"), cdr2.buf);
+			if (rename(cdr.buf, cdr2.buf))
+				warning(_("%s: unable to rename obsolete "
+					  "conffile '%.250s' to '%.250s': %s"),
+					  pkg_name(pkg, pnaw_nonambig),
+					  cdr.buf, cdr2.buf, strerror(errno));
+		} else {
+			printf(_("Removing obsolete conffile %.250s ...\n"),
+				 cdr.buf);
+			if (unlink(cdr.buf) && errno != ENOENT)
+				warning(_("%s: failed to remove '%.250s': %s"),
+					  pkg_name(pkg, pnaw_nonambig), cdr.buf,
+					  strerror(errno));
+		}
+		return;
+	}
+
 	strcpy(cdr2rest, DPKGNEWEXT);
 	/* If the .dpkg-new file is no longer there, ignore this one. */
 	if (lstat(cdr2.buf, &stab)) {
diff --git a/src/help.c b/src/help.c
index f8f8a80708..e075ff2015 100644
--- a/src/help.c
+++ b/src/help.c
@@ -228,7 +228,7 @@ dir_has_conffiles(struct fsys_namenode *file, struct pkginfo *pkg)
         pkg_name(pkg, pnaw_always));
   namelen = strlen(file->name);
   for (conff= pkg->installed.conffiles; conff; conff= conff->next) {
-      if (conff->obsolete)
+      if (conff->obsolete || conff->remove_on_upgrade)
         continue;
       if (strncmp(file->name, conff->name, namelen) == 0 &&
           strlen(conff->name) > namelen && conff->name[namelen] == '/') {
diff --git a/src/unpack.c b/src/unpack.c
index ee453a88d1..953296abf6 100644
--- a/src/unpack.c
+++ b/src/unpack.c
@@ -317,6 +317,14 @@ pkg_deconfigure_others(struct pkginfo *pkg)
   }
 }
 
+static char *rtrim_space(char *line_end, const char *const line_start) {
+    while (line_end > line_start && c_isspace(line_end[-1]))
+      --line_end;
+    if (line_end > line_start)
+      *line_end = '\0';
+    return line_end;
+}
+
 /**
  * Read the conffiles, and copy the hashes across.
  */
@@ -343,6 +351,8 @@ deb_parse_conffiles(struct pkginfo *pkg, const char *control_conffiles,
     struct fsys_namenode_list *newconff;
     struct conffile *searchconff;
     char *p;
+    char *flags_pos;
+    enum fsys_namenode_flags confflags = FNNF_NEW_CONFF;
 
     p = conffilenamebuf + strlen(conffilenamebuf);
     if (p == conffilenamebuf)
@@ -350,11 +360,29 @@ deb_parse_conffiles(struct pkginfo *pkg, const char *control_conffiles,
     if (p[-1] != '\n')
       ohshit(_("conffile name '%s' is too long, or missing final newline"),
              conffilenamebuf);
-    while (p > conffilenamebuf && c_isspace(p[-1]))
-      --p;
+    p = rtrim_space(p, conffilenamebuf);
     if (p == conffilenamebuf)
       continue;
-    *p = '\0';
+    flags_pos = strstr(conffilenamebuf, ":");
+    if (flags_pos) {
+      /* rtrim_space will insert a NUL byte and split the string for us */
+      p = rtrim_space(flags_pos, conffilenamebuf);
+      if (p == conffilenamebuf) {
+        ohshit(_("no conffile name before flags in line '%.250s'"),
+               conffilenamebuf);
+      }
+      do {
+        flags_pos++;
+      } while (c_isspace(*flags_pos));
+      /* FIXME: how do we handle it if file /is/ present along with this flag? */
+      if (strcmp(flags_pos, "remove-on-upgrade") == 0) {
+        confflags |= FNNF_RM_CONFF_ON_UPGRADE;
+        confflags &= ~FNNF_NEW_CONFF;
+      } else {
+        ohshit(_("unknown conffile flag '%.250s' for conffile '%.250s'"),
+               flags_pos, conffilenamebuf);
+      }
+    }
 
     namenode = fsys_hash_find_node(conffilenamebuf, 0);
     namenode->oldhash = NEWCONFFILEFLAG;
@@ -408,7 +436,7 @@ deb_parse_conffiles(struct pkginfo *pkg, const char *control_conffiles,
       debug(dbg_conff, "process_archive conffile '%s' no package, no hash",
             newconff->namenode->name);
     }
-    newconff->namenode->flags |= FNNF_NEW_CONFF;
+    newconff->namenode->flags |= confflags;
   }
 
   if (ferror(conff))
@@ -569,6 +597,7 @@ pkg_remove_old_files(struct pkginfo *pkg,
     struct fsys_namenode *usenode;
 
     if ((namenode->flags & FNNF_NEW_CONFF) ||
+        (namenode->flags & FNNF_RM_CONFF_ON_UPGRADE) ||
         (namenode->flags & FNNF_NEW_INARCHIVE))
       continue;
 
@@ -788,6 +817,8 @@ pkg_update_fields(struct pkginfo *pkg, struct fsys_namenode_queue *newconffiles)
     newiconff->name = nfstrsave(cfile->namenode->name);
     newiconff->hash = nfstrsave(cfile->namenode->oldhash);
     newiconff->obsolete = !!(cfile->namenode->flags & FNNF_OBS_CONFF);
+    newiconff->remove_on_upgrade = !!(
+        cfile->namenode->flags & FNNF_RM_CONFF_ON_UPGRADE);
     *iconffileslastp = newiconff;
     iconffileslastp = &newiconff->next;
   }
-- 
2.26.2

