From d72277030321e90c08086a91023dfdbe3b6e986c Mon Sep 17 00:00:00 2001
From: Niels Thykier <niels@thykier.net>
Date: Wed, 22 Apr 2020 15:38:46 +0000
Subject: [PATCH 1/5] 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   | 30 ++++++++++++++++++++++++++++--
 lib/dpkg/dpkg-db.h |  1 +
 lib/dpkg/fsys.h    |  2 ++
 src/configure.c    | 28 ++++++++++++++++++++++++++++
 src/unpack.c       | 39 +++++++++++++++++++++++++++++++++++----
 5 files changed, 94 insertions(+), 6 deletions(-)

diff --git a/dpkg-deb/build.c b/dpkg-deb/build.c
index 3317b517a6..d4031d784e 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,16 +280,40 @@ 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)) {
-      if (errno == ENOENT) {
+      if (errno == ENOENT && !remove_on_upgrade) {
         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
+      } else if (!remove_on_upgrade)
         ohshite(_("conffile '%.250s' is not stattable"), 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/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/configure.c b/src/configure.c
index 12f290451b..e19ba5bdb0 100644
--- a/src/configure.c
+++ b/src/configure.c
@@ -401,6 +401,34 @@ 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) {
+		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/unpack.c b/src/unpack.c
index ee453a88d1..416d9d1bff 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

