Greetings dpkg devs,

I was trying to understand how dpkg-maintscript-helper's rm_conffile and mv_conffile commands handle (or not) config files that are not conffiles -- either shipped but not in the Conffiles status field, or not shipped and generated during postinst configure (e.g., ucf). The documentation is not clear, so I wrote some tests and documentation improvements.

Attached is a series of patches (apply them by running `git am *.patch`) that do the following:

  * Add extensive tests for rm_conffile and mv_conffile.
* Document how rm_conffile and mv_conffile handle (or not) files not listed in the package's conffiles.
  * Fix some (minor) issues uncovered by the tests.

I can use 'git send-email' to send a separate email per commit if that would be preferred; I didn't want to spam the mailing list without knowing your preferences first.

Feedback would be appreciated.

Thanks,
Richard
From 104ca2345078fb390941f2dace021b2f92c8f4cd Mon Sep 17 00:00:00 2001
From: Richard Hansen <rhan...@rhansen.org>
Date: Wed, 12 Feb 2025 17:16:02 -0500
Subject: [PATCH 01/10] tests: Move .gitignore out of dpkginst

Now the initial state of dpkginst is empty which makes it easier for
tests to ensure that everything was cleaned up after purge.  For
example:

    # Ensure that purge removed everything.
    test -z "$$(ls -A '$(DPKG_INSTDIR)')"
---
 tests/.gitignore          |  2 ++
 tests/Test.mk             | 13 ++++++++++++-
 tests/dpkginst/.gitignore |  1 -
 3 files changed, 14 insertions(+), 2 deletions(-)
 delete mode 100644 tests/dpkginst/.gitignore

diff --git a/tests/.gitignore b/tests/.gitignore
index 7498a3d16..3d3853d94 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -1,3 +1,5 @@
 !Makefile
 *.deb
 /.pkg-tests.conf
+/dpkginst/
+dpkginst.stamp
diff --git a/tests/Test.mk b/tests/Test.mk
index a830d5d49..a13a508b8 100644
--- a/tests/Test.mk
+++ b/tests/Test.mk
@@ -110,14 +110,25 @@ pkg_is_not_installed = $(call stdout_has,$(PKG_STATUS) $(1) 2>/dev/null, ok not-
 pkg_status_is = $(call stdout_is,$(PKG_STATUS) $(1),$(2))
 pkg_field_is = $(call stdout_is,$(DPKG_QUERY) -f '$${$(2)}' -W $(1),$(3))
 
-%.deb: %
+# Avoid using $(DPKG_INSTDIR) as a dependency because it would be immediately
+# expanded, making it impossible for individual tests to override DPKG_INSTDIR.
+# (Variable expansion in a recipe is deferred.)
+dpkginst.stamp: | dpkginst
+	touch '$@'
+.PHONY: dpkginst
+dpkginst:
+	mkdir -p '$(DPKG_INSTDIR)'
+
+%.deb: % dpkginst.stamp
 	$(DPKG_BUILD_DEB) $< $@
 
 TEST_CASES :=
 
 build: build-hook $(DEB)
+build-hook: dpkginst.stamp
 
 test: build test-case test-clean
+test-case: dpkginst.stamp
 
 clean: clean-hook
 	$(RM) $(DEB)
diff --git a/tests/dpkginst/.gitignore b/tests/dpkginst/.gitignore
deleted file mode 100644
index 72e8ffc0d..000000000
--- a/tests/dpkginst/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*
-- 
2.48.1

From 7108bde0838f3393f347fa502a8e05454cfe1a15 Mon Sep 17 00:00:00 2001
From: Richard Hansen <rhan...@rhansen.org>
Date: Thu, 13 Feb 2025 02:46:44 -0500
Subject: [PATCH 02/10] tests: dpkg-m-h: New rm_conffile tests to improve
 coverage

This commit adds exhaustive (I hope) test cases for rm_conffile.

Current state of rm_conffile:

Removing a conffile: dpkg-maintscript-helper works as expected.

Removing a shipped-but-non-conffile: dpkg-maintscript-helper works as
expected, except:

  * When upgrading, dpkg-m-h treats all non-conffiles as if they were
    modified, so it creates unnecessary .dpkg-bak files even for
    unmodified files.  (These unnecessary .dpkg-bak files are not
    created when the user does remove-then-install instead of upgrade
    because dpkg deletes the non-conffile files before the new
    version's postinst runs, not after.)  A TODO comment was added.

  * When upgrading, dpkg-m-h backs up modified files.  I consider this
    behavior to be a feature, not a bug, but it differs from the
    behavior of mv_conffile (which does not preserve modifications to
    non-conffiles) and from the remove-then-install case (where it is
    impossible to make a backup because dpkg deletes the modified
    non-conffile before preinst has the opportunity to make a backup).

Removing a non-shipped file: dpkg-maintscript-helper silently ignores
any attempt to remove a file not listed in `dpkg-query -L`, as
expected.  As a consequence, dpkg-m-h cannot be used to remove a
config file created during postinst by ucf or via debconf answers.
---
 tests/Makefile                                |   1 +
 tests/t-maintscript-rm/Makefile               | 226 ++++++++++++++++++
 .../pkg-rm-0/DEBIAN/conffiles                 |   1 +
 .../t-maintscript-rm/pkg-rm-0/DEBIAN/control  |   7 +
 .../t-maintscript-rm/pkg-rm-0/DEBIAN/postinst |   8 +
 tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postrm |   7 +
 tests/t-maintscript-rm/pkg-rm-0/conf          |   1 +
 tests/t-maintscript-rm/pkg-rm-0/nonconf       |   1 +
 .../t-maintscript-rm/pkg-rm-1/DEBIAN/control  |   7 +
 .../t-maintscript-rm/pkg-rm-1/DEBIAN/postinst |   9 +
 tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postrm |   5 +
 .../t-maintscript-rm/pkg-rm-1/DEBIAN/preinst  |   8 +
 12 files changed, 281 insertions(+)
 create mode 100644 tests/t-maintscript-rm/Makefile
 create mode 100644 tests/t-maintscript-rm/pkg-rm-0/DEBIAN/conffiles
 create mode 100644 tests/t-maintscript-rm/pkg-rm-0/DEBIAN/control
 create mode 100755 tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postinst
 create mode 100755 tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postrm
 create mode 100644 tests/t-maintscript-rm/pkg-rm-0/conf
 create mode 100644 tests/t-maintscript-rm/pkg-rm-0/nonconf
 create mode 100644 tests/t-maintscript-rm/pkg-rm-1/DEBIAN/control
 create mode 100755 tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postinst
 create mode 100755 tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postrm
 create mode 100755 tests/t-maintscript-rm/pkg-rm-1/DEBIAN/preinst

diff --git a/tests/Makefile b/tests/Makefile
index bdd9dd318..ff66e108f 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -41,6 +41,7 @@ ifdef DPKG_AS_ROOT
 TESTS_PASS += t-unpack-device
 endif
 TESTS_PASS += t-maintscript-leak
+TESTS_PASS += t-maintscript-rm
 TESTS_PASS += t-filtering
 TESTS_PASS += t-depends
 TESTS_PASS += t-dir-leftover-parents
diff --git a/tests/t-maintscript-rm/Makefile b/tests/t-maintscript-rm/Makefile
new file mode 100644
index 000000000..d1d342fc1
--- /dev/null
+++ b/tests/t-maintscript-rm/Makefile
@@ -0,0 +1,226 @@
+TESTS_DEB := pkg-rm-0 pkg-rm-1
+
+include ../Test.mk
+
+# Explanation of the cases:
+#   - conf: pkg-rm-0 ships this and lists it in its conffiles
+#   - nonconf: pkg-rm-0 ships this but does NOT list it in its conffiles
+#   - unowned: pkg-rm-0 does NOT ship this file; it is generated in postinst
+cases = conf nonconf unowned
+
+define check-clean =
+out=$$(ls -A '$(DPKG_INSTDIR)') || exit 1 && test -z "$$out" || { \
+  printf %s\\n "install directory "'$(DPKG_INSTDIR)'" not clean:" "$$out" >&2; \
+  exit 1; \
+}
+endef
+
+define prepare-unmodified =
+$(check-clean)
+$(DPKG_INSTALL) pkg-rm-0.deb
+$(foreach case,$(cases),$\
+test -f '$(DPKG_INSTDIR)/$(case)'
+test "$$(cat '$(DPKG_INSTDIR)/$(case)')" = "original"
+)
+endef
+
+define prepare-modified =
+$(prepare-unmodified)
+$(foreach case,$(cases),$\
+$(BEROOT) sh -c "echo modified >'"'$(DPKG_INSTDIR)/$(case)'"'"
+test -f '$(DPKG_INSTDIR)/$(case)'
+test "$$(cat '$(DPKG_INSTDIR)/$(case)')" = "modified"
+)
+endef
+
+define prepare-abort =
+$(BEROOT) touch '$(DPKG_INSTDIR)'/fail-preinst
+$(DPKG_INSTALL) pkg-rm-1.deb || true
+$(BEROOT) rm -f '$(DPKG_INSTDIR)'/fail-preinst
+endef
+
+define clean-up =
+# Cleaning up leftover /$(1) which should have contents $(2)
+test -f '$(DPKG_INSTDIR)/$(1)'
+test "$$(cat '$(DPKG_INSTDIR)/$(1)')" = '$(2)'
+$(BEROOT) rm -f '$(DPKG_INSTDIR)/$(1)'
+endef
+
+define check-nexists =
+test ! -f '$(DPKG_INSTDIR)/$(1)'
+endef
+
+define check-exists =
+test -f '$(DPKG_INSTDIR)/$(1)'
+test "$$(cat '$(DPKG_INSTDIR)/$(1)')" = '$(2)'
+endef
+
+define check-config-file =
+# $(1)($(2))
+$(if $(3),$\
+  $(call check-exists,$(2),$(3)),$\
+  $(call check-nexists,$(2)))
+$(call check-nexists,$(2).dpkg-backup)
+$(if $(4),$\
+  $(call check-exists,$(2).dpkg-bak,$(4)),$\
+  $(call check-nexists,$(2).dpkg-bak))
+$(call check-nexists,$(2).dpkg-remove)
+
+endef
+
+# Asserts that $(1) and backup files do not exist.
+check-removed-clean = \
+  $(call check-config-file,check-removed-clean,$(1),,)
+
+# Asserts that case $(1) with "modified" contents was removed, with
+# $(1).dpkg-bak containing $(2).
+check-removed-with-backup = \
+  $(call check-config-file,check-removed-with-backup,$(1),,$(2))
+
+# Asserts that the attempt to remove $(1) was ignored; it still exists with
+# contents $(2).  No backup files exist.
+check-noop = \
+  $(call check-config-file,check-noop,$(1),$(2),)
+
+TEST_CASES += test-install
+test-install:
+	$(check-clean)
+	$(DPKG_INSTALL) pkg-rm-1.deb
+	$(call check-removed-clean,conf)
+	$(call check-removed-clean,nonconf)
+	$(call check-removed-clean,unowned)
+	$(DPKG_PURGE) pkg-rm
+	$(check-clean)
+
+TEST_CASES += test-unmodified-upgrade
+test-unmodified-upgrade:
+	$(prepare-unmodified)
+	$(DPKG_INSTALL) pkg-rm-1.deb
+	$(call check-removed-clean,conf)
+	# TODO: rm_conffile mistakenly treats unmodified files that are shipped
+	# but not in conffiles as if they were modified.
+	$(call check-removed-with-backup,nonconf,original)
+	# Attempts to remove unowned files are ignored, and pkg-rm-1
+	# intentionally does not clean up any unowned files installed by
+	# pkg-rm-0.
+	$(call check-noop,unowned,original)
+	$(DPKG_PURGE) pkg-rm
+	$(call clean-up,unowned,original)
+	$(check-clean)
+
+TEST_CASES += test-modified-upgrade
+test-modified-upgrade:
+	$(prepare-modified)
+	$(DPKG_INSTALL) pkg-rm-1.deb
+	$(call check-removed-with-backup,conf,modified)
+	$(call check-removed-with-backup,nonconf,modified)
+	# Attempts to remove unowned files are ignored, and pkg-rm-1
+	# intentionally does not clean up any unowned files installed by
+	# pkg-rm-0.
+	$(call check-noop,unowned,modified)
+	$(DPKG_PURGE) pkg-rm
+	$(call clean-up,unowned,modified)
+	$(check-clean)
+
+TEST_CASES += test-unmodified-remove-install
+test-unmodified-remove-install:
+	$(prepare-unmodified)
+	$(DPKG_REMOVE) pkg-rm
+	$(DPKG_INSTALL) pkg-rm-1.deb
+	$(call check-removed-clean,conf)
+	$(call check-removed-clean,nonconf)
+	# Attempts to remove unowned files are ignored, and pkg-rm-1
+	# intentionally does not clean up any unowned files installed by
+	# pkg-rm-0.
+	$(call check-noop,unowned,original)
+	$(DPKG_PURGE) pkg-rm
+	$(call clean-up,unowned,original)
+	$(check-clean)
+
+TEST_CASES += test-modified-remove-install
+test-modified-remove-install:
+	$(prepare-modified)
+	$(DPKG_REMOVE) pkg-rm
+	$(DPKG_INSTALL) pkg-rm-1.deb
+	$(call check-removed-with-backup,conf,modified)
+	# When an old version of a package is removed before the new version is
+	# installed (as opposed to upgrading from the old to the new), it is
+	# impossible for dpkg-m-h to back up the modified contents of a file
+	# that was shipped but not listed in conffiles.  This is because dpkg
+	# unconditionally deletes each non-conffile when removing a package, and
+	# does so without making a backup even if the file is modified.  This
+	# removal happens before the new version's preinst runs; contrast this
+	# with package upgrades where the new version's preinst runs before
+	# the old version's files are deleted.
+	$(call check-removed-clean,nonconf)
+	# Attempts to remove unowned files are ignored, and pkg-rm-1
+	# intentionally does not clean up any unowned files installed by
+	# pkg-rm-0.
+	$(call check-noop,unowned,modified)
+	$(DPKG_PURGE) pkg-rm
+	$(call clean-up,unowned,modified)
+	$(check-clean)
+
+TEST_CASES += test-unmodified-abort
+test-unmodified-abort:
+	$(prepare-unmodified)
+	$(prepare-abort)
+	$(call check-noop,conf,original)
+	$(call check-noop,nonconf,original)
+	$(call check-noop,unowned,original)
+	$(DPKG_PURGE) pkg-rm
+	$(check-clean)
+
+TEST_CASES += test-modified-abort
+test-modified-abort:
+	$(prepare-modified)
+	$(prepare-abort)
+	$(call check-noop,conf,modified)
+	$(call check-noop,nonconf,modified)
+	$(call check-noop,unowned,modified)
+	$(DPKG_PURGE) pkg-rm
+	$(check-clean)
+
+TEST_CASES += test-unmodified-unpack-purge
+test-unmodified-unpack-purge:
+	$(prepare-unmodified)
+	$(DPKG_UNPACK) pkg-rm-1.deb
+# We don't really care about the state of the files between unpacking and
+# purging:
+#   - There's no defined semantics for the 'unpacked' state.
+#   - A package is not expected to be in the 'unpacked' state for long on a real
+#     system, so it's better to test the possible final states.
+#   - The state of the files depends on dpkg implementation choices, which
+#     should be tested elsewhere.
+	$(DPKG_PURGE) pkg-rm
+	# Attempts to remove unowned files are ignored, and pkg-rm-1
+	# intentionally does not clean up any unowned files installed by
+	# pkg-rm-0.
+	$(call clean-up,unowned,original)
+	$(check-clean)
+
+TEST_CASES += test-modified-unpack-purge
+test-modified-unpack-purge:
+	$(prepare-modified)
+	$(DPKG_UNPACK) pkg-rm-1.deb
+# We don't really care about the state of the files between unpacking and
+# purging:
+#   - There's no defined semantics for the 'unpacked' state.
+#   - A package is not expected to be in the 'unpacked' state for long on a real
+#     system, so it's better to test the possible final states.
+#   - The state of the files depends on dpkg implementation choices, which
+#     should be tested elsewhere.
+	$(DPKG_PURGE) pkg-rm
+	# Attempts to remove unowned files are ignored, and pkg-rm-1
+	# intentionally does not clean up any unowned files installed by
+	# pkg-rm-0.
+	$(call clean-up,unowned,modified)
+	$(check-clean)
+
+.PHONY: $(TEST_CASES)
+
+test-case: $(TEST_CASES)
+
+test-clean:
+	$(DPKG_PURGE) pkg-rm
+	$(check-clean)
diff --git a/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/conffiles b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/conffiles
new file mode 100644
index 000000000..c9a5c234a
--- /dev/null
+++ b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/conffiles
@@ -0,0 +1 @@
+/conf
diff --git a/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/control b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/control
new file mode 100644
index 000000000..8f2f0d3c2
--- /dev/null
+++ b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/control
@@ -0,0 +1,7 @@
+Package: pkg-rm
+Version: 0
+Section: test
+Priority: extra
+Maintainer: Dpkg Developers <debian-dpkg@lists.debian.org>
+Architecture: all
+Description: test package - rm_conffile
diff --git a/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postinst b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postinst
new file mode 100755
index 000000000..0669af0ba
--- /dev/null
+++ b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postinst
@@ -0,0 +1,8 @@
+#!/bin/sh
+set -e
+case $1 in
+  configure)
+    # Simulate creation of the config file via ucf or debconf.
+    echo original >$DPKG_ROOT/unowned
+    ;;
+esac
diff --git a/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postrm b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postrm
new file mode 100755
index 000000000..47c82c688
--- /dev/null
+++ b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postrm
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -e
+case $1 in
+  purge)
+    rm -f "$DPKG_ROOT"/unowned
+    ;;
+esac
diff --git a/tests/t-maintscript-rm/pkg-rm-0/conf b/tests/t-maintscript-rm/pkg-rm-0/conf
new file mode 100644
index 000000000..4b48deed3
--- /dev/null
+++ b/tests/t-maintscript-rm/pkg-rm-0/conf
@@ -0,0 +1 @@
+original
diff --git a/tests/t-maintscript-rm/pkg-rm-0/nonconf b/tests/t-maintscript-rm/pkg-rm-0/nonconf
new file mode 100644
index 000000000..4b48deed3
--- /dev/null
+++ b/tests/t-maintscript-rm/pkg-rm-0/nonconf
@@ -0,0 +1 @@
+original
diff --git a/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/control b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/control
new file mode 100644
index 000000000..dae93a783
--- /dev/null
+++ b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/control
@@ -0,0 +1,7 @@
+Package: pkg-rm
+Version: 1
+Section: test
+Priority: extra
+Maintainer: Dpkg Developers <debian-dpkg@lists.debian.org>
+Architecture: all
+Description: test package - rm_conffile
diff --git a/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postinst b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postinst
new file mode 100755
index 000000000..84501c341
--- /dev/null
+++ b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postinst
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -e
+# dpkg-maintscript-helper's rm_conffile command doesn't support unowned files,
+# so the postinst script would normally delete the unowned file itself and not
+# attempt to use rm_conffile.  rm_conffile is intentionally misused here to test
+# its behavior when given an unowned file.
+for tc in conf nonconf unowned; do
+  dpkg-maintscript-helper rm_conffile "/$tc" 0 -- "$@"
+done
diff --git a/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postrm b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postrm
new file mode 100755
index 000000000..344ea141e
--- /dev/null
+++ b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postrm
@@ -0,0 +1,5 @@
+#!/bin/sh
+set -e
+for tc in conf nonconf unowned; do
+  dpkg-maintscript-helper rm_conffile "/$tc" 0 -- "$@"
+done
diff --git a/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/preinst b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/preinst
new file mode 100755
index 000000000..c3ea10bd8
--- /dev/null
+++ b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/preinst
@@ -0,0 +1,8 @@
+#!/bin/sh
+set -e
+for tc in conf nonconf unowned; do
+  dpkg-maintscript-helper rm_conffile "/$tc" 0 -- "$@"
+done
+if [ -e "$DPKG_ROOT/fail-preinst" ]; then
+  exit 1
+fi
-- 
2.48.1

From 430ab82615903c336aab972eaed5cf9b0cbc4c3d Mon Sep 17 00:00:00 2001
From: Richard Hansen <rhan...@rhansen.org>
Date: Thu, 30 Jan 2025 20:53:16 -0500
Subject: [PATCH 03/10] tests: dpkg-m-h: New mv_conffile tests to improve
 coverage

This commit adds exhaustive (I hope) test cases for mv_conffile.

Current state of mv_conffile:

Moving a conffile: dpkg-maintscript-helper works as expected
regardless of whether the destination is another conffile path, a
shipped-but-non-conffile path, or a non-shipped path (e.g., normally
created during postinst by ucf or via debconf answers).  However,
there are a couple of minor purge bugs, marked with TODO comments:

  * If (a) the src conffile is unmodified and (b) the new package is
    unpacked but never configured (or configuration failed before
    mv_conffile was invoked) and (c) the package is purged, then
    dpkg-maintscript-helper will not delete the .dpkg-remove file it
    created during preinst.

  * If (a) the src conffile is modified and (b) the dst path is a
    non-shipped path and (c) the dst path exists before mv_conffile is
    invoked in postinst and (d) the package is purged, then the
    .dpkg-new file created by dpkg-m-h during postinst is not deleted.
    (This is not a problem for the conffile and
    shipped-but-non-conffile cases thanks to reliance on
    undocumented(?) dpkg behavior.)

Moving a shipped-but-non-conffile: dpkg-maintscript-helper does not
preserve the contents of a modified src file if it was shipped in the
previously installed version but was not listed as a conffile.  This
could be problematic if the previously installed version erroneously
omitted the file from conffiles.  If the user performs
remove-then-install instead of upgrade, it is impossible for dpkg-m-h
to preserve modifications because dpkg deletes the modified
non-conffile before the new version's preinst runs.  However, for
upgrades it is technically possible to preserve modifications.  Doing
so would align with the current behavior of rm_conffile (which creates
a backup of a modified non-conffile) but would not be consistent with
the remove-then-install behavior.

Moving a non-shipped file: dpkg-maintscript-helper silently ignores
any attempt to move a file not listed in `dpkg-query -L`, as expected.
As a consequence, dpkg-m-h cannot be used to rename a config file
created during postinst by ucf or via debconf answers.
---
 tests/Makefile                       |   1 +
 tests/t-maintscript-mv/.gitignore    |   4 +
 tests/t-maintscript-mv/Makefile      | 391 +++++++++++++++++++++++++++
 tests/t-maintscript-mv/control.in    |   7 +
 tests/t-maintscript-mv/postinst-0.in |  10 +
 tests/t-maintscript-mv/postinst-1.in |  17 ++
 tests/t-maintscript-mv/postrm-0.in   |   9 +
 tests/t-maintscript-mv/postrm-1.in   |   9 +
 tests/t-maintscript-mv/preinst-1.in  |   8 +
 9 files changed, 456 insertions(+)
 create mode 100644 tests/t-maintscript-mv/.gitignore
 create mode 100644 tests/t-maintscript-mv/Makefile
 create mode 100644 tests/t-maintscript-mv/control.in
 create mode 100755 tests/t-maintscript-mv/postinst-0.in
 create mode 100755 tests/t-maintscript-mv/postinst-1.in
 create mode 100755 tests/t-maintscript-mv/postrm-0.in
 create mode 100755 tests/t-maintscript-mv/postrm-1.in
 create mode 100755 tests/t-maintscript-mv/preinst-1.in

diff --git a/tests/Makefile b/tests/Makefile
index ff66e108f..8414dec21 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -41,6 +41,7 @@ ifdef DPKG_AS_ROOT
 TESTS_PASS += t-unpack-device
 endif
 TESTS_PASS += t-maintscript-leak
+TESTS_PASS += t-maintscript-mv
 TESTS_PASS += t-maintscript-rm
 TESTS_PASS += t-filtering
 TESTS_PASS += t-depends
diff --git a/tests/t-maintscript-mv/.gitignore b/tests/t-maintscript-mv/.gitignore
new file mode 100644
index 000000000..baca1433c
--- /dev/null
+++ b/tests/t-maintscript-mv/.gitignore
@@ -0,0 +1,4 @@
+/control-0.in
+/control-1.in
+/pkg-mv-0/
+/pkg-mv-1/
diff --git a/tests/t-maintscript-mv/Makefile b/tests/t-maintscript-mv/Makefile
new file mode 100644
index 000000000..79a11412b
--- /dev/null
+++ b/tests/t-maintscript-mv/Makefile
@@ -0,0 +1,391 @@
+TESTS_DEB := pkg-mv-0 pkg-mv-1
+
+include ../Test.mk
+
+define newline =
+
+
+endef
+
+# Source file (installed by pkg-mv-0.deb) to destination file (installed by
+# pkg-mv-1.deb) combinations:
+#   - conf-to-*: src file is shipped and listed in pkg-mv-0's conffiles
+#   - nonconf-to-*: src file is shipped in pkg-mv-0 but is not in conffiles
+#   - unowned-to-*: src file is created in pkg-mv-0's postinst, not shipped
+#   - *-to-conf: dst file is shipped and listed in pkg-mv-1's conffiles
+#   - *-to-nonconf: dst file is shipped in pkg-mv-1 but is not in conffiles
+#   - *-to-unowned: dst file is created in pkg-mv-1's postinst, not shipped
+cases = \
+  $(foreach src,conf nonconf unowned,$\
+    $(foreach dst,conf nonconf unowned,$\
+      $(src)-to-$(dst)))
+
+$(foreach case,$(cases),$\
+  $(if $(filter conf-to-%,$(case)),$(eval conffiles-0 += $(case)-src))$\
+  $(if $(filter nonconf-to-%,$(case)),$(eval nonconffiles-0 += $(case)-src))$\
+  $(if $(filter unowned-to-%,$(case)),$(eval unowned-0 += $(case)-src))$\
+  $(if $(filter %-to-conf,$(case)),$(eval conffiles-1 += $(case)-dst))$\
+  $(if $(filter %-to-nonconf,$(case)),$(eval nonconffiles-1 += $(case)-dst))$\
+  $(if $(filter %-to-unowned,$(case)),$(eval unowned-1 += $(case)-dst)))
+
+controlfiles-0 = conffiles control postinst postrm
+controlfiles-1 = conffiles control preinst postinst postrm
+
+# $(1) is the package version number (either 0 or 1)
+define pkg-mv-template =
+controlfiles-src-$(1) = $$(controlfiles-$(1):%=pkg-mv-$(1)/DEBIAN/%)
+shipped-$(1) = $$(conffiles-$(1)) $$(nonconffiles-$(1))
+shipped-src-$(1) = $$(shipped-$(1):%=pkg-mv-$(1)/%)
+
+$$(shipped-src-$(1)): | pkg-mv-$(1)
+
+pkg-mv-$(1).deb: \
+  pkg-mv-$(1)/DEBIAN/conffiles \
+  $$(shipped-src-$(1)) \
+  $$(controlfiles-src-$(1))
+
+pkg-mv-$(1)/DEBIAN/%: %-$(1).in Makefile | pkg-mv-$(1)/DEBIAN
+	rm -f '$$@'.tmp
+	sed \
+	  -e 's|%cases%|$$(cases)|g' \
+	  -e 's|%unowned%|$$(unowned-$(v))|g' \
+	  -e 's|%version%|$(v)|g' \
+	  '$$<' >'$$@'.tmp
+	case '$$<' in control-*|conffiles-*);; *) chmod +x '$$@'.tmp;; esac
+	mv '$$@'.tmp '$$@'
+
+%-$(1).in: %.in
+	cp '$$<' '$$@'
+
+pkg-mv-$(1)/DEBIAN/conffiles: Makefile | pkg-mv-$(1)/DEBIAN
+	rm -f '$$@'.tmp
+	$$(foreach cf,$$(conffiles-$(1)),$\
+	  printf %s\\n '/$$(cf)' >>'$$@'.tmp$$(newline))
+	mv '$$@'.tmp '$$@'
+
+pkg-mv-$(1)/DEBIAN: | pkg-mv-$(1)
+
+pkg-mv-$(1)/DEBIAN pkg-mv-$(1):
+	mkdir -p '$$@'
+endef
+
+$(foreach v,0 1,$(eval $(call pkg-mv-template,$(v))))
+
+$(shipped-src-0):
+	echo old >'$@'
+
+$(shipped-src-1):
+	echo new >'$@'
+
+define check-clean =
+out=$$(ls -A '$(DPKG_INSTDIR)') || exit 1 && test -z "$$out" || { \
+  printf %s\\n "install directory "'$(DPKG_INSTDIR)'" not clean:" "$$out" >&2; \
+  exit 1; \
+}
+endef
+
+define prepare-unmodified =
+$(check-clean)
+$(DPKG_INSTALL) pkg-mv-0.deb
+$(foreach case,$(cases),$\
+test -f '$(DPKG_INSTDIR)/$(case)'-src
+test "$$(cat '$(DPKG_INSTDIR)/$(case)'-src)" = "old"
+)
+endef
+
+define prepare-modified =
+$(prepare-unmodified)
+$(foreach case,$(cases),$\
+$(BEROOT) sh -c "echo modified >'$(DPKG_INSTDIR)/$(case)'-src"
+test -f '$(DPKG_INSTDIR)/$(case)'-src
+test "$$(cat '$(DPKG_INSTDIR)/$(case)'-src)" = "modified"
+)
+endef
+
+define prepare-abort =
+$(BEROOT) touch '$(DPKG_INSTDIR)'/fail-preinst
+$(DPKG_INSTALL) pkg-mv-1.deb || true
+$(BEROOT) rm -f '$(DPKG_INSTDIR)'/fail-preinst
+endef
+
+define clean-up =
+# Cleaning up leftover /$(1) which should have contents $(2)
+test -f '$(DPKG_INSTDIR)/$(1)'
+test "$$(cat '$(DPKG_INSTDIR)/$(1)')" = '$(2)'
+$(BEROOT) rm -f '$(DPKG_INSTDIR)/$(1)'
+endef
+
+define check-nexists =
+test ! -f '$(DPKG_INSTDIR)/$(1)'
+endef
+
+define check-exists =
+test -f '$(DPKG_INSTDIR)/$(1)'
+test "$$(cat '$(DPKG_INSTDIR)/$(1)')" = '$(2)'
+endef
+
+define check-config-files =
+# $(1)($(2))
+$(if $(3),$\
+  $(call check-exists,$(2)-src,$(3)),$\
+  $(call check-nexists,$(2)-src))
+$(call check-nexists,$(2)-src.dpkg-remove)
+$(if $(4),$\
+  $(call check-exists,$(2)-dst,$(4)),$\
+  $(call check-nexists,$(2)-dst))
+$(if $(5),$\
+  $(call check-exists,$(2)-dst.dpkg-new,$(5)),$\
+  $(call check-nexists,$(2)-dst.dpkg-new))
+
+endef
+
+# Asserts that case $(1) was cleanly installed/upgraded with "new" contents.
+check-clean-installed = \
+  $(call check-config-files,check-clean-installed,$(1),,new,)
+
+# Asserts that the attempt to upgrade case $(1) was ignored, dst has "new"
+# contents, and src still exists with contents $(2).
+check-upgrade-ignored = \
+  $(call check-config-files,check-upgrade-ignored,$(1),$(2),new,)
+
+# Asserts that case $(1) with "modified" contents was cleanly upgraded.
+check-upgraded-modified = \
+  $(call check-config-files,check-upgraded-modified,$(1),,modified,new)
+
+# Asserts that no changes were made for case $(1); src still contains $(2).
+check-noop = $(call check-config-files,check-noop,$(1),$(2),,)
+
+TEST_CASES += test-install
+test-install:
+	$(check-clean)
+	$(DPKG_INSTALL) pkg-mv-1.deb
+	$(foreach case,$(cases),$(call check-clean-installed,$(case)))
+	$(DPKG_PURGE) pkg-mv
+	# mv_conffile doesn't support unowned src or dst files, and the test
+	# package intentionally does not clean them up (so that mv_conffile's
+	# behavior with unowned files can be tested here).
+	$(call clean-up,conf-to-unowned-dst,new)
+	$(call clean-up,nonconf-to-unowned-dst,new)
+	$(call clean-up,unowned-to-unowned-dst,new)
+	$(check-clean)
+
+TEST_CASES += test-unmodified-upgrade
+test-unmodified-upgrade:
+	$(prepare-unmodified)
+	$(DPKG_INSTALL) pkg-mv-1.deb
+	$(call check-clean-installed,conf-to-conf)
+	$(call check-clean-installed,conf-to-nonconf)
+	$(call check-clean-installed,conf-to-unowned)
+	$(call check-clean-installed,nonconf-to-conf)
+	$(call check-clean-installed,nonconf-to-nonconf)
+	$(call check-clean-installed,nonconf-to-unowned)
+	# Attempts to move unowned files are ignored, and pkg-mv-1 intentionally
+	# does not clean up any unowned files installed by pkg-mv-0.
+	$(call check-upgrade-ignored,unowned-to-conf,old)
+	$(call check-upgrade-ignored,unowned-to-nonconf,old)
+	$(call check-upgrade-ignored,unowned-to-unowned,old)
+
+	######## Test cleanup.
+	$(DPKG_PURGE) pkg-mv
+	# mv_conffile doesn't support unowned src or dst files, and the test
+	# package intentionally does not clean them up (so that mv_conffile's
+	# behavior with unowned files can be tested here).
+	$(call clean-up,unowned-to-conf-src,old)
+	$(call clean-up,unowned-to-nonconf-src,old)
+	$(call clean-up,unowned-to-unowned-src,old)
+	$(call clean-up,conf-to-unowned-dst,new)
+	$(call clean-up,nonconf-to-unowned-dst,new)
+	$(call clean-up,unowned-to-unowned-dst,new)
+	$(check-clean)
+
+TEST_CASES += test-modified-upgrade
+test-modified-upgrade:
+	$(prepare-modified)
+	$(DPKG_INSTALL) pkg-mv-1.deb
+	$(call check-upgraded-modified,conf-to-conf)
+	$(call check-upgraded-modified,conf-to-nonconf)
+	$(call check-upgraded-modified,conf-to-unowned)
+	# TODO: dpkg-m-h could check each nonconf-to-*-src against the package's
+	# md5sums control file and back it up if modified.  That would prevent
+	# loss of configuration data if the nonconf file was erroneously omitted
+	# from conffiles in the previous version of the package.
+	$(call check-clean-installed,nonconf-to-conf)
+	$(call check-clean-installed,nonconf-to-nonconf)
+	$(call check-clean-installed,nonconf-to-unowned)
+	# Attempts to move unowned files are ignored, and pkg-mv-1 intentionally
+	# does not clean up any unowned files installed by pkg-mv-0.
+	$(call check-upgrade-ignored,unowned-to-conf,modified)
+	$(call check-upgrade-ignored,unowned-to-nonconf,modified)
+	$(call check-upgrade-ignored,unowned-to-unowned,modified)
+
+	######## Test cleanup.
+	$(DPKG_PURGE) pkg-mv
+	# mv_conffile doesn't support unowned src or dst files, and the test
+	# package intentionally does not clean them up (so that mv_conffile's
+	# behavior with unowned files can be tested here).
+	$(call clean-up,unowned-to-conf-src,modified)
+	$(call clean-up,unowned-to-nonconf-src,modified)
+	$(call clean-up,unowned-to-unowned-src,modified)
+	$(call clean-up,conf-to-unowned-dst,modified)
+	# TODO: dpkg deletes the modified src before postinst has the chance to
+	# move it to dst (hence "new" instead of "modified").
+	$(call clean-up,nonconf-to-unowned-dst,new)
+	# Attempts to move unowned files are ignored, so the modified src does
+	# not propagate to the dst (hence "new" instead of "modified").
+	$(call clean-up,unowned-to-unowned-dst,new)
+	# TODO: Either ignore attempts to move to an unowned dst or delete the
+	# .dpkg-new in postrm purge.
+	$(call clean-up,conf-to-unowned-dst.dpkg-new,new)
+	$(check-clean)
+
+TEST_CASES += test-unmodified-remove-install
+test-unmodified-remove-install:
+	$(prepare-unmodified)
+	$(DPKG_REMOVE) pkg-mv
+	$(DPKG_INSTALL) pkg-mv-1.deb
+	$(call check-clean-installed,conf-to-conf)
+	$(call check-clean-installed,conf-to-nonconf)
+	$(call check-clean-installed,conf-to-unowned)
+	$(call check-clean-installed,nonconf-to-conf)
+	$(call check-clean-installed,nonconf-to-nonconf)
+	$(call check-clean-installed,nonconf-to-unowned)
+	# Attempts to move unowned files are ignored, and pkg-mv-1 intentionally
+	# does not clean up any unowned files installed by pkg-mv-0.
+	$(call check-upgrade-ignored,unowned-to-conf,old)
+	$(call check-upgrade-ignored,unowned-to-nonconf,old)
+	$(call check-upgrade-ignored,unowned-to-unowned,old)
+
+	######## Test cleanup.
+	$(DPKG_PURGE) pkg-mv
+	# mv_conffile doesn't support unowned src or dst files, and the test
+	# package intentionally does not clean them up (so that mv_conffile's
+	# behavior with unowned files can be tested here).
+	$(call clean-up,unowned-to-conf-src,old)
+	$(call clean-up,unowned-to-nonconf-src,old)
+	$(call clean-up,unowned-to-unowned-src,old)
+	$(call clean-up,conf-to-unowned-dst,new)
+	$(call clean-up,nonconf-to-unowned-dst,new)
+	$(call clean-up,unowned-to-unowned-dst,new)
+	$(check-clean)
+
+TEST_CASES += test-modified-remove-install
+test-modified-remove-install:
+	$(prepare-modified)
+	$(DPKG_REMOVE) pkg-mv
+	$(DPKG_INSTALL) pkg-mv-1.deb
+	$(call check-upgraded-modified,conf-to-conf)
+	$(call check-upgraded-modified,conf-to-nonconf)
+	$(call check-upgraded-modified,conf-to-unowned)
+	# When an old version of a package is removed before the new version is
+	# installed (as opposed to upgrading from the old to the new), it is
+	# impossible for dpkg-m-h to preserve the modified contents of a file
+	# that was shipped but not listed in conffiles.  This is because dpkg
+	# unconditionally deletes each non-conffile when removing a package, and
+	# does so without making a backup even if the file is modified.  This
+	# removal happens before the new version's preinst runs; contrast this
+	# with package upgrades where the new version's preinst runs before the
+	# old version's files are deleted.
+	$(call check-clean-installed,nonconf-to-conf)
+	$(call check-clean-installed,nonconf-to-nonconf)
+	$(call check-clean-installed,nonconf-to-unowned)
+	# Attempts to move unowned files are ignored, and pkg-mv-1 intentionally
+	# does not clean up any unowned files installed by pkg-mv-0.
+	$(call check-upgrade-ignored,unowned-to-conf,modified)
+	$(call check-upgrade-ignored,unowned-to-nonconf,modified)
+	$(call check-upgrade-ignored,unowned-to-unowned,modified)
+
+	######## Test cleanup.
+	$(DPKG_PURGE) pkg-mv
+	# mv_conffile doesn't support unowned src or dst files, and the test
+	# package intentionally does not clean them up (so that mv_conffile's
+	# behavior with unowned files can be tested here).
+	$(call clean-up,unowned-to-conf-src,modified)
+	$(call clean-up,unowned-to-nonconf-src,modified)
+	$(call clean-up,unowned-to-unowned-src,modified)
+	$(call clean-up,conf-to-unowned-dst,modified)
+	# dpkg-m-h can't preserve the modified contents of nonconf files when
+	# the package is removed then installed (hence "new" instead of
+	# "modified").
+	$(call clean-up,nonconf-to-unowned-dst,new)
+	# Attempts to move unowned files are ignored, so the modified src does
+	# not propagate to the dst (hence "new" instead of "modified").
+	$(call clean-up,unowned-to-unowned-dst,new)
+	# TODO: Either ignore attempts to move to an unowned dst or delete the
+	# .dpkg-new in postrm purge.
+	$(call clean-up,conf-to-unowned-dst.dpkg-new,new)
+	$(check-clean)
+
+TEST_CASES += test-unmodified-abort
+test-unmodified-abort:
+	$(prepare-unmodified)
+	$(prepare-abort)
+	$(foreach case,$(cases),$(call check-noop,$(case),old))
+	$(DPKG_PURGE) pkg-mv
+	$(check-clean)
+
+TEST_CASES += test-modified-abort
+test-modified-abort:
+	$(prepare-modified)
+	$(prepare-abort)
+	$(foreach case,$(cases),$(call check-noop,$(case),modified))
+	$(DPKG_PURGE) pkg-mv
+	$(check-clean)
+
+TEST_CASES += test-unmodified-unpack-purge
+test-unmodified-unpack-purge:
+	$(prepare-unmodified)
+	$(DPKG_UNPACK) pkg-mv-1.deb
+# We don't really care about the state of the files between unpacking and
+# purging:
+#   - The semantics of the 'unpacked' state is out of scope for this test.
+#   - A package is not expected to be in the 'unpacked' state for long on a real
+#     system, so it's better to test the possible final states.
+#   - The state of the files depends on dpkg implementation choices, which
+#     should be tested elsewhere.
+	$(DPKG_PURGE) pkg-mv
+	# mv_conffile doesn't support unowned src or dst files, and the test
+	# package intentionally does not clean them up (so that mv_conffile's
+	# behavior with unowned files can be tested here).  Note: pkg-mv-1
+	# didn't get a chance to create the *-to-unowned-dst files so they don't
+	# need to be cleaned up.
+	$(call clean-up,unowned-to-conf-src,old)
+	$(call clean-up,unowned-to-nonconf-src,old)
+	$(call clean-up,unowned-to-unowned-src,old)
+	# TODO: Delete the .dpkg-remove files in postrm purge.
+	$(call clean-up,conf-to-conf-src.dpkg-remove,old)
+	$(call clean-up,conf-to-nonconf-src.dpkg-remove,old)
+	$(call clean-up,conf-to-unowned-src.dpkg-remove,old)
+	$(check-clean)
+
+TEST_CASES += test-modified-unpack-purge
+test-modified-unpack-purge:
+	$(prepare-modified)
+	$(DPKG_UNPACK) pkg-mv-1.deb
+# We don't really care about the state of the files between unpacking and
+# purging:
+#   - The semantics of the 'unpacked' state is out of scope for this test.
+#   - A package is not expected to be in the 'unpacked' state for long on a real
+#     system, so it's better to test the possible final states.
+#   - The state of the files depends on dpkg implementation choices, which
+#     should be tested elsewhere.
+	$(DPKG_PURGE) pkg-mv
+	# mv_conffile doesn't support unowned src or dst files, and the test
+	# package intentionally does not clean them up (so that mv_conffile's
+	# behavior with unowned files can be tested here).  Note: pkg-mv-1
+	# didn't get a chance to create the *-to-unowned-dst files so they don't
+	# need to be cleaned up.
+	$(call clean-up,unowned-to-conf-src,modified)
+	$(call clean-up,unowned-to-nonconf-src,modified)
+	$(call clean-up,unowned-to-unowned-src,modified)
+	$(check-clean)
+
+.PHONY: $(TEST_CASES)
+
+test-case: $(TEST_CASES)
+
+test-clean:
+	$(DPKG_PURGE) pkg-mv
+	$(check-clean)
+
+clean-hook:
+	rm -rf $(TESTS_DEB)
diff --git a/tests/t-maintscript-mv/control.in b/tests/t-maintscript-mv/control.in
new file mode 100644
index 000000000..0b9a37ff3
--- /dev/null
+++ b/tests/t-maintscript-mv/control.in
@@ -0,0 +1,7 @@
+Package: pkg-mv
+Version: %version%
+Section: test
+Priority: extra
+Maintainer: Dpkg Developers <debian-dpkg@lists.debian.org>
+Architecture: all
+Description: test package - mv_conffile
diff --git a/tests/t-maintscript-mv/postinst-0.in b/tests/t-maintscript-mv/postinst-0.in
new file mode 100755
index 000000000..5592eae6d
--- /dev/null
+++ b/tests/t-maintscript-mv/postinst-0.in
@@ -0,0 +1,10 @@
+#!/bin/sh
+set -e
+case $1 in
+  configure)
+    # Simulate creation of the config file via ucf or debconf.
+    for f in %unowned%; do
+      echo old >$DPKG_ROOT/$f
+    done
+    ;;
+esac
diff --git a/tests/t-maintscript-mv/postinst-1.in b/tests/t-maintscript-mv/postinst-1.in
new file mode 100755
index 000000000..7280f6333
--- /dev/null
+++ b/tests/t-maintscript-mv/postinst-1.in
@@ -0,0 +1,17 @@
+#!/bin/sh
+set -e
+case $1 in
+  configure)
+    # Simulate creation of the new config file via ucf or debconf.
+    for f in %unowned%; do
+      echo new >$DPKG_ROOT/$f
+    done
+    ;;
+esac
+# dpkg-maintscript-helper's mv_conffile command doesn't support unowned src
+# files, so the postinst script would normally rename the unowned file itself
+# and not attempt to use mv_conffile.  mv_conffile is intentionally misused here
+# to test its behavior when given an unowned src.
+for tc in %cases%; do
+  dpkg-maintscript-helper mv_conffile "/$tc-src" "/$tc-dst" 0 -- "$@"
+done
diff --git a/tests/t-maintscript-mv/postrm-0.in b/tests/t-maintscript-mv/postrm-0.in
new file mode 100755
index 000000000..ea13ab288
--- /dev/null
+++ b/tests/t-maintscript-mv/postrm-0.in
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -e
+case $1 in
+  purge)
+    for f in %unowned%; do
+      rm -f "$DPKG_ROOT/$f"
+    done
+    ;;
+esac
diff --git a/tests/t-maintscript-mv/postrm-1.in b/tests/t-maintscript-mv/postrm-1.in
new file mode 100755
index 000000000..8f30a8c04
--- /dev/null
+++ b/tests/t-maintscript-mv/postrm-1.in
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -e
+# dpkg-maintscript-helper's mv_conffile command doesn't support unowned dst
+# files, so the postrm script would normally delete the unowned file itself
+# during purge.  mv_conffile is intentionally misused here to test its behavior
+# when given an unowned dst.
+for tc in %cases%; do
+  dpkg-maintscript-helper mv_conffile "/$tc-src" "/$tc-dst" 0 -- "$@"
+done
diff --git a/tests/t-maintscript-mv/preinst-1.in b/tests/t-maintscript-mv/preinst-1.in
new file mode 100755
index 000000000..72b9b3cfa
--- /dev/null
+++ b/tests/t-maintscript-mv/preinst-1.in
@@ -0,0 +1,8 @@
+#!/bin/sh
+set -e
+for tc in %cases%; do
+  dpkg-maintscript-helper mv_conffile "/$tc-src" "/$tc-dst" 0 -- "$@"
+done
+if [ -e "$DPKG_ROOT/fail-preinst" ]; then
+  exit 1
+fi
-- 
2.48.1

From d5070c6e57feb412018270f7d8b9032c0775c5b3 Mon Sep 17 00:00:00 2001
From: Richard Hansen <rhan...@rhansen.org>
Date: Wed, 29 Jan 2025 19:23:46 -0500
Subject: [PATCH 04/10] man: dpkg-m-h: {rm,mv}_conffile is only meant for
 ${Conffiles}

---
 man/dpkg-maintscript-helper.pod | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/man/dpkg-maintscript-helper.pod b/man/dpkg-maintscript-helper.pod
index ee938e911..2adaeba2a 100644
--- a/man/dpkg-maintscript-helper.pod
+++ b/man/dpkg-maintscript-helper.pod
@@ -153,6 +153,17 @@ Z<>
 =back
 
 I<conffile> is the filename of the conffile to remove.
+I<conffile> must not be shipped in the version of the package invoking
+B<rm_conffile>.
+This command has no effect (no error, no warning) if either I<conffile> does not
+exist on the user's system or if I<conffile> is not listed in the files
+installed by the package (i.e., the output of B<dpkg-query --listfiles>).
+In particular, this command does nothing if I<conffile> was created in
+B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to
+L<debconf(7)> questions).
+The behavior is undefined if I<conffile> exists and is listed in the files
+installed by the package but is not listed in the package's B<Conffiles> status
+field.
 
 Current implementation: in the B<preinst>, it checks if the conffile
 was modified and renames it either to I<conffile>B<.dpkg-remove> (if not
@@ -189,6 +200,24 @@ Z<>
 I<old-conffile> and I<new-conffile> are the old and new name of the
 conffile to rename.
 
+I<old-conffile> must not be shipped in the version of the package invoking
+B<mv_conffile>.
+This command has no effect (no error, no warning) if either I<old-conffile> does
+not exist on the user's system or if I<old-conffile> is not listed in the files
+installed by the package (i.e., the output of B<dpkg-query --listfiles>).
+In particular, this command does nothing if I<old-conffile> was created in
+B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to
+L<debconf(7)> questions).
+The behavior is undefined if I<old-conffile> exists and is listed in the files
+installed by the package but is not listed in the package's B<Conffiles> status
+field.
+
+The behavior is undefined if I<new-conffile> is not listed in the package's
+I<conffiles> control file (i.e., I<DEBIAN/conffiles> during package creation).
+In particular, the behavior is undefined if I<new-conffile> is created in
+B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to
+L<debconf(7)> questions).
+
 Current implementation: the B<preinst> checks if the conffile has been
 modified, if yes it's left on place otherwise it's renamed to
 I<old-conffile>B<.dpkg-remove>.
-- 
2.48.1

From 4745f5825d922f6f6427c5ad9e67fb5264b6157d Mon Sep 17 00:00:00 2001
From: Richard Hansen <rhan...@rhansen.org>
Date: Mon, 24 Feb 2025 02:22:37 -0500
Subject: [PATCH 05/10] man: dpkg-m-h: mv_conffile doesn't prompt if orig
 conffile changed

---
 man/dpkg-maintscript-helper.pod | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/man/dpkg-maintscript-helper.pod b/man/dpkg-maintscript-helper.pod
index 2adaeba2a..5ba5423f1 100644
--- a/man/dpkg-maintscript-helper.pod
+++ b/man/dpkg-maintscript-helper.pod
@@ -227,6 +227,13 @@ to I<new-conffile> if I<old-conffile> is still available.
 On
 abort-upgrade/abort-install, the B<postrm> renames
 I<old-conffile>B<.dpkg-remove> back to I<old-conffile> if required.
+B<mv_conffile> does not currently prompt the user to update a moved conffile if
+the new unmodified version differs from the old unmodified version (see L<bug
+#711598|https://bugs.debian.org/711598>).
+If the package installs the new unmodified config file to I<new-conffile> before
+it invokes B<mv_conffile> in B<postinst> during configuration, B<mv_conffile>
+will rename it to I<new-conffile>B<.dpkg-new> for user convenience or for
+post-processing in B<postinst> after B<mv_conffile> returns.
 
 =head1 SYMLINK AND DIRECTORY SWITCHES
 
-- 
2.48.1

From 54f42ef220df185a93024aacc3b1305ff0b60180 Mon Sep 17 00:00:00 2001
From: Richard Hansen <rhan...@rhansen.org>
Date: Wed, 29 Jan 2025 19:27:51 -0500
Subject: [PATCH 06/10] dpkg-m-h: Improve robustness of conffile md5 extraction

This avoids problems if the conffile name contains regex
metacharacters.
---
 src/dpkg-maintscript-helper.sh | 21 +++++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/src/dpkg-maintscript-helper.sh b/src/dpkg-maintscript-helper.sh
index c7de6edf9..155c017ce 100755
--- a/src/dpkg-maintscript-helper.sh
+++ b/src/dpkg-maintscript-helper.sh
@@ -98,9 +98,8 @@ prepare_rm_conffile() {
   ensure_package_owns_file "$PACKAGE" "$CONFFILE" || return 0
 
   local md5sum old_md5sum
+  old_md5sum=$(conffile_md5 "$PACKAGE" "$CONFFILE")
   md5sum="$(md5sum "$DPKG_ROOT$CONFFILE" | sed -e 's/ .*//')"
-  old_md5sum="$(dpkg-query -W -f='${Conffiles}' "$PACKAGE" | \
-    sed -n -e "\\'^ $CONFFILE ' { s/ obsolete$//; s/.* //; p }")"
   if [ "$md5sum" != "$old_md5sum" ]; then
     mv -f "$DPKG_ROOT$CONFFILE" "$DPKG_ROOT$CONFFILE.dpkg-backup"
   else
@@ -212,9 +211,8 @@ prepare_mv_conffile() {
   ensure_package_owns_file "$PACKAGE" "$CONFFILE" || return 0
 
   local md5sum old_md5sum
+  old_md5sum=$(conffile_md5 "$PACKAGE" "$CONFFILE")
   md5sum="$(md5sum "$DPKG_ROOT$CONFFILE" | sed -e 's/ .*//')"
-  old_md5sum="$(dpkg-query -W -f='${Conffiles}' "$PACKAGE" | \
-    sed -n -e "\\'^ $CONFFILE ' { s/ obsolete$//; s/.* //; p }")"
   if [ "$md5sum" = "$old_md5sum" ]; then
     mv -f "$DPKG_ROOT$CONFFILE" "$DPKG_ROOT$CONFFILE.dpkg-remove"
   fi
@@ -526,6 +524,21 @@ validate_optional_version() {
   fi
 }
 
+conffile_md5() {
+  local out line
+  out=$(dpkg-query -W -f '${Conffiles}' "$1")
+  [ -n "$out" ] || return 0
+  # Setting IFS to <space><tab> strips leading and trailing space from the line.
+  while IFS=' 	' read -r line; do
+    line=${line% obsolete}
+    [ "${line% *}" = "$2" ] || continue
+    printf %s\\n "${line##* }"
+    return 0
+  done <<EOF
+${out}
+EOF
+}
+
 ensure_package_owns_file() {
   local PACKAGE="$1"
   local FILE="$2"
-- 
2.48.1

From c9d4f6bfe8a690cc44edb204786fc5f7b506024e Mon Sep 17 00:00:00 2001
From: Richard Hansen <rhan...@rhansen.org>
Date: Wed, 29 Jan 2025 19:32:43 -0500
Subject: [PATCH 07/10] dpkg-m-h: rm_conffile, mv_conffile: Skip if not a
 conffile

Before this commit, if the user ran `rm_conffile` or `mv_conffile` on
a non-conffile belonging to the package, it would treat the file as if
it was a modified conffile:
  * `ensure_package_owns_file` returns true on non-conffiles because
    it is based on `dpkg-query -L`, which includes non-conffiles.
  * `rm_conffile` and `mv_conffile` did not return if the file was
    missing from `Conffiles`.  Instead, they behaved as if the file's
    original md5 was the empty string, which always compares unequal
    to any real md5.

Now `rm_conffile` and `mv_conffile` silently ignore non-conffiles
because they test file existence in the `Conffiles` field, not
existence in the complete list of package files.
---
 man/dpkg-maintscript-helper.pod | 14 ++++----------
 src/dpkg-maintscript-helper.sh  | 17 +++++++++++------
 tests/t-maintscript-rm/Makefile | 10 ++++++----
 3 files changed, 21 insertions(+), 20 deletions(-)

diff --git a/man/dpkg-maintscript-helper.pod b/man/dpkg-maintscript-helper.pod
index 5ba5423f1..129a3963b 100644
--- a/man/dpkg-maintscript-helper.pod
+++ b/man/dpkg-maintscript-helper.pod
@@ -156,14 +156,11 @@ I<conffile> is the filename of the conffile to remove.
 I<conffile> must not be shipped in the version of the package invoking
 B<rm_conffile>.
 This command has no effect (no error, no warning) if either I<conffile> does not
-exist on the user's system or if I<conffile> is not listed in the files
-installed by the package (i.e., the output of B<dpkg-query --listfiles>).
+exist on the user's system or if I<conffile> is not listed in the package's
+B<Conffiles> status field.
 In particular, this command does nothing if I<conffile> was created in
 B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to
 L<debconf(7)> questions).
-The behavior is undefined if I<conffile> exists and is listed in the files
-installed by the package but is not listed in the package's B<Conffiles> status
-field.
 
 Current implementation: in the B<preinst>, it checks if the conffile
 was modified and renames it either to I<conffile>B<.dpkg-remove> (if not
@@ -203,14 +200,11 @@ conffile to rename.
 I<old-conffile> must not be shipped in the version of the package invoking
 B<mv_conffile>.
 This command has no effect (no error, no warning) if either I<old-conffile> does
-not exist on the user's system or if I<old-conffile> is not listed in the files
-installed by the package (i.e., the output of B<dpkg-query --listfiles>).
+not exist on the user's system or if I<old-conffile> is not listed in the
+package's B<Conffiles> status field.
 In particular, this command does nothing if I<old-conffile> was created in
 B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to
 L<debconf(7)> questions).
-The behavior is undefined if I<old-conffile> exists and is listed in the files
-installed by the package but is not listed in the package's B<Conffiles> status
-field.
 
 The behavior is undefined if I<new-conffile> is not listed in the package's
 I<conffiles> control file (i.e., I<DEBIAN/conffiles> during package creation).
diff --git a/src/dpkg-maintscript-helper.sh b/src/dpkg-maintscript-helper.sh
index 155c017ce..8ef957a0e 100755
--- a/src/dpkg-maintscript-helper.sh
+++ b/src/dpkg-maintscript-helper.sh
@@ -95,10 +95,10 @@ prepare_rm_conffile() {
   local PACKAGE="$2"
 
   [ -e "$DPKG_ROOT$CONFFILE" ] || return 0
-  ensure_package_owns_file "$PACKAGE" "$CONFFILE" || return 0
 
   local md5sum old_md5sum
   old_md5sum=$(conffile_md5 "$PACKAGE" "$CONFFILE")
+  [ -n "$old_md5sum" ] || return 0
   md5sum="$(md5sum "$DPKG_ROOT$CONFFILE" | sed -e 's/ .*//')"
   if [ "$md5sum" != "$old_md5sum" ]; then
     mv -f "$DPKG_ROOT$CONFFILE" "$DPKG_ROOT$CONFFILE.dpkg-backup"
@@ -125,7 +125,7 @@ abort_rm_conffile() {
   local CONFFILE="$1"
   local PACKAGE="$2"
 
-  ensure_package_owns_file "$PACKAGE" "$CONFFILE" || return 0
+  package_owns_conffile "$PACKAGE" "$CONFFILE" || return 0
 
   if [ -e "$DPKG_ROOT$CONFFILE.dpkg-remove" ]; then
     echo "Reinstalling $DPKG_ROOT$CONFFILE that was moved away"
@@ -208,10 +208,9 @@ prepare_mv_conffile() {
 
   [ -e "$DPKG_ROOT$CONFFILE" ] || return 0
 
-  ensure_package_owns_file "$PACKAGE" "$CONFFILE" || return 0
-
   local md5sum old_md5sum
   old_md5sum=$(conffile_md5 "$PACKAGE" "$CONFFILE")
+  [ -n "$old_md5sum" ] || return 0
   md5sum="$(md5sum "$DPKG_ROOT$CONFFILE" | sed -e 's/ .*//')"
   if [ "$md5sum" = "$old_md5sum" ]; then
     mv -f "$DPKG_ROOT$CONFFILE" "$DPKG_ROOT$CONFFILE.dpkg-remove"
@@ -226,7 +225,7 @@ finish_mv_conffile() {
   rm -f "$DPKG_ROOT$OLDCONFFILE.dpkg-remove"
 
   [ -e "$DPKG_ROOT$OLDCONFFILE" ] || return 0
-  ensure_package_owns_file "$PACKAGE" "$OLDCONFFILE" || return 0
+  package_owns_conffile "$PACKAGE" "$OLDCONFFILE" || return 0
 
   echo "Preserving user changes to $DPKG_ROOT$NEWCONFFILE (renamed from $DPKG_ROOT$OLDCONFFILE)..."
   if [ -e "$DPKG_ROOT$NEWCONFFILE" ]; then
@@ -239,7 +238,7 @@ abort_mv_conffile() {
   local CONFFILE="$1"
   local PACKAGE="$2"
 
-  ensure_package_owns_file "$PACKAGE" "$CONFFILE" || return 0
+  package_owns_conffile "$PACKAGE" "$CONFFILE" || return 0
 
   if [ -e "$DPKG_ROOT$CONFFILE.dpkg-remove" ]; then
     echo "Reinstalling $DPKG_ROOT$CONFFILE that was moved away"
@@ -539,6 +538,12 @@ ${out}
 EOF
 }
 
+package_owns_conffile() {
+  local md5
+  md5=$(conffile_md5 "$@")
+  [ -n "$md5" ] && true
+}
+
 ensure_package_owns_file() {
   local PACKAGE="$1"
   local FILE="$2"
diff --git a/tests/t-maintscript-rm/Makefile b/tests/t-maintscript-rm/Makefile
index d1d342fc1..efe2a7682 100644
--- a/tests/t-maintscript-rm/Makefile
+++ b/tests/t-maintscript-rm/Makefile
@@ -97,9 +97,7 @@ test-unmodified-upgrade:
 	$(prepare-unmodified)
 	$(DPKG_INSTALL) pkg-rm-1.deb
 	$(call check-removed-clean,conf)
-	# TODO: rm_conffile mistakenly treats unmodified files that are shipped
-	# but not in conffiles as if they were modified.
-	$(call check-removed-with-backup,nonconf,original)
+	$(call check-removed-clean,nonconf)
 	# Attempts to remove unowned files are ignored, and pkg-rm-1
 	# intentionally does not clean up any unowned files installed by
 	# pkg-rm-0.
@@ -113,7 +111,11 @@ test-modified-upgrade:
 	$(prepare-modified)
 	$(DPKG_INSTALL) pkg-rm-1.deb
 	$(call check-removed-with-backup,conf,modified)
-	$(call check-removed-with-backup,nonconf,modified)
+	# TODO: dpkg-m-h could check the nonconf file against the package's
+	# md5sums control file and back it up if modified.  That would prevent
+	# loss of configuration data if the nonconf file was erroneously omitted
+	# from conffiles in the previous version of the package.
+	$(call check-removed-clean,nonconf)
 	# Attempts to remove unowned files are ignored, and pkg-rm-1
 	# intentionally does not clean up any unowned files installed by
 	# pkg-rm-0.
-- 
2.48.1

From fe33acbf7b74bedf1cd9e4b2c31aa82d9eb056cd Mon Sep 17 00:00:00 2001
From: Richard Hansen <rhan...@rhansen.org>
Date: Fri, 31 Jan 2025 17:55:29 -0500
Subject: [PATCH 08/10] dpkg-m-h: mv_conffile: Delete .dpkg-remove on purge

---
 src/dpkg-maintscript-helper.sh  | 5 +++++
 tests/t-maintscript-mv/Makefile | 4 ----
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/dpkg-maintscript-helper.sh b/src/dpkg-maintscript-helper.sh
index 8ef957a0e..b328a99c3 100755
--- a/src/dpkg-maintscript-helper.sh
+++ b/src/dpkg-maintscript-helper.sh
@@ -190,6 +190,11 @@ mv_conffile() {
     fi
     ;;
   postrm)
+    if [ "$1" = "purge" ]; then
+      # $OLDCONFFILE.dpkg-remove is deleted here in case the package was
+      # unpacked but never configured before it was purged.
+      rm -f "$DPKG_ROOT$OLDCONFFILE.dpkg-remove"
+    fi
     if [ "$1" = "abort-install" -o "$1" = "abort-upgrade" ] &&
        [ -n "$2" ] &&
        dpkg --compare-versions -- "$2" le-nl "$LASTVERSION"; then
diff --git a/tests/t-maintscript-mv/Makefile b/tests/t-maintscript-mv/Makefile
index 79a11412b..c15c8aad4 100644
--- a/tests/t-maintscript-mv/Makefile
+++ b/tests/t-maintscript-mv/Makefile
@@ -351,10 +351,6 @@ test-unmodified-unpack-purge:
 	$(call clean-up,unowned-to-conf-src,old)
 	$(call clean-up,unowned-to-nonconf-src,old)
 	$(call clean-up,unowned-to-unowned-src,old)
-	# TODO: Delete the .dpkg-remove files in postrm purge.
-	$(call clean-up,conf-to-conf-src.dpkg-remove,old)
-	$(call clean-up,conf-to-nonconf-src.dpkg-remove,old)
-	$(call clean-up,conf-to-unowned-src.dpkg-remove,old)
 	$(check-clean)
 
 TEST_CASES += test-modified-unpack-purge
-- 
2.48.1

From 59fb217c5f1aaa7395bc3dcb6abab519a63c348d Mon Sep 17 00:00:00 2001
From: Richard Hansen <rhan...@rhansen.org>
Date: Sat, 1 Feb 2025 02:36:56 -0500
Subject: [PATCH 09/10] dpkg-m-h: mv_conffile: Delete .dpkg-new on purge

---
 src/dpkg-maintscript-helper.sh  | 8 ++++++++
 tests/t-maintscript-mv/Makefile | 6 ------
 2 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/src/dpkg-maintscript-helper.sh b/src/dpkg-maintscript-helper.sh
index b328a99c3..0eea4f434 100755
--- a/src/dpkg-maintscript-helper.sh
+++ b/src/dpkg-maintscript-helper.sh
@@ -194,6 +194,14 @@ mv_conffile() {
       # $OLDCONFFILE.dpkg-remove is deleted here in case the package was
       # unpacked but never configured before it was purged.
       rm -f "$DPKG_ROOT$OLDCONFFILE.dpkg-remove"
+      # dpkg deletes *.dpkg-new corresponding to shipped files (both conffiles
+      # and non-conffiles) during purge, but $NEWCONFFILE might not be a shipped
+      # file (e.g., it might be created during postinst via ucf(1) or using
+      # debconf(7) answers).
+      rm -f "$DPKG_ROOT$NEWCONFFILE.dpkg-new"
+      # $NEWCONFFILE is not deleted during purge here: dpkg itself deletes
+      # shipped files, and the package's postrm script is expected to delete
+      # non-shipped (created during postinst) config files itself.
     fi
     if [ "$1" = "abort-install" -o "$1" = "abort-upgrade" ] &&
        [ -n "$2" ] &&
diff --git a/tests/t-maintscript-mv/Makefile b/tests/t-maintscript-mv/Makefile
index c15c8aad4..b56e18143 100644
--- a/tests/t-maintscript-mv/Makefile
+++ b/tests/t-maintscript-mv/Makefile
@@ -233,9 +233,6 @@ test-modified-upgrade:
 	# Attempts to move unowned files are ignored, so the modified src does
 	# not propagate to the dst (hence "new" instead of "modified").
 	$(call clean-up,unowned-to-unowned-dst,new)
-	# TODO: Either ignore attempts to move to an unowned dst or delete the
-	# .dpkg-new in postrm purge.
-	$(call clean-up,conf-to-unowned-dst.dpkg-new,new)
 	$(check-clean)
 
 TEST_CASES += test-unmodified-remove-install
@@ -310,9 +307,6 @@ test-modified-remove-install:
 	# Attempts to move unowned files are ignored, so the modified src does
 	# not propagate to the dst (hence "new" instead of "modified").
 	$(call clean-up,unowned-to-unowned-dst,new)
-	# TODO: Either ignore attempts to move to an unowned dst or delete the
-	# .dpkg-new in postrm purge.
-	$(call clean-up,conf-to-unowned-dst.dpkg-new,new)
 	$(check-clean)
 
 TEST_CASES += test-unmodified-abort
-- 
2.48.1

From 61f719ec116520c8fa93ca1e28a239422eed3a30 Mon Sep 17 00:00:00 2001
From: Richard Hansen <rhan...@rhansen.org>
Date: Mon, 24 Feb 2025 02:37:49 -0500
Subject: [PATCH 10/10] man: dpkg-m-h: mv_conffile now supports non-conffile
 destination

---
 man/dpkg-maintscript-helper.pod | 25 ++++++++++++-------------
 1 file changed, 12 insertions(+), 13 deletions(-)

diff --git a/man/dpkg-maintscript-helper.pod b/man/dpkg-maintscript-helper.pod
index 129a3963b..6ae29575d 100644
--- a/man/dpkg-maintscript-helper.pod
+++ b/man/dpkg-maintscript-helper.pod
@@ -190,12 +190,12 @@ scripts:
 
 Z<>
  dpkg-maintscript-helper mv_conffile \
-    I<old-conffile> I<new-conffile> I<prior-version> I<package> -- "$@"
+    I<old-conffile> I<new-configfile> I<prior-version> I<package> -- "$@"
 
 =back
 
-I<old-conffile> and I<new-conffile> are the old and new name of the
-conffile to rename.
+I<old-conffile> is the name of the conffile to rename, and I<new-configfile> is
+its new name.
 
 I<old-conffile> must not be shipped in the version of the package invoking
 B<mv_conffile>.
@@ -206,28 +206,27 @@ In particular, this command does nothing if I<old-conffile> was created in
 B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to
 L<debconf(7)> questions).
 
-The behavior is undefined if I<new-conffile> is not listed in the package's
-I<conffiles> control file (i.e., I<DEBIAN/conffiles> during package creation).
-In particular, the behavior is undefined if I<new-conffile> is created in
-B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to
-L<debconf(7)> questions).
+The package is not required to ship I<new-configfile> or create it during
+configuration before invoking B<mv_conffile>.
+The package is responsible for ensuring that I<new-configfile> does not belong
+to another package.
 
 Current implementation: the B<preinst> checks if the conffile has been
 modified, if yes it's left on place otherwise it's renamed to
 I<old-conffile>B<.dpkg-remove>.
 On configuration, the B<postinst>
 removes I<old-conffile>B<.dpkg-remove> and renames I<old-conffile>
-to I<new-conffile> if I<old-conffile> is still available.
+to I<new-configfile> if I<old-conffile> is still available.
 On
 abort-upgrade/abort-install, the B<postrm> renames
 I<old-conffile>B<.dpkg-remove> back to I<old-conffile> if required.
 B<mv_conffile> does not currently prompt the user to update a moved conffile if
 the new unmodified version differs from the old unmodified version (see L<bug
 #711598|https://bugs.debian.org/711598>).
-If the package installs the new unmodified config file to I<new-conffile> before
-it invokes B<mv_conffile> in B<postinst> during configuration, B<mv_conffile>
-will rename it to I<new-conffile>B<.dpkg-new> for user convenience or for
-post-processing in B<postinst> after B<mv_conffile> returns.
+If the package installs the new unmodified config file to I<new-configfile>
+before it invokes B<mv_conffile> in B<postinst> during configuration,
+B<mv_conffile> will rename it to I<new-configfile>B<.dpkg-new> for user
+convenience or for post-processing in B<postinst> after B<mv_conffile> returns.
 
 =head1 SYMLINK AND DIRECTORY SWITCHES
 
-- 
2.48.1

Attachment: OpenPGP_signature.asc
Description: OpenPGP digital signature

Reply via email to