public inbox for gentoo-portage-dev@lists.gentoo.org
 help / color / mirror / Atom feed
From: zmedico@gentoo.org
To: gentoo-portage-dev@lists.gentoo.org
Cc: Zac Medico <zmedico@gentoo.org>
Subject: [gentoo-portage-dev] [PATCH 3/3] CONFIG_PROTECT: protect symlinks, bug #485598
Date: Sun, 26 Oct 2014 04:12:16 -0700	[thread overview]
Message-ID: <1414321936-22851-3-git-send-email-zmedico@gentoo.org> (raw)
In-Reply-To: <1414321936-22851-1-git-send-email-zmedico@gentoo.org>

From: Zac Medico <zmedico@gentoo.org>

Users may not want some symlinks to get clobbered, so protect them
with CONFIG_PROTECT. Changes were required in the dblink.mergeme method
and the new_protect_filename function. The unit tests demonstrate
operation in many different scenarios. For example:

 * regular file replaces regular file
 * regular file replaces symlink
 * regular file replaces directory
 * symlink replaces symlink
 * symlink replaces regular file
 * symlink replaces directory
 * directory replaces regular file
 * directory replaces symlink

X-Gentoo-Bug: 485598
X-Gentoo-Bug-URL: https://bugs.gentoo.org/show_bug.cgi?id=485598
---
 pym/portage/dbapi/vartree.py                    | 255 ++++++++++++---------
 pym/portage/tests/emerge/test_config_protect.py | 292 ++++++++++++++++++++++++
 pym/portage/util/__init__.py                    |  35 ++-
 3 files changed, 463 insertions(+), 119 deletions(-)
 create mode 100644 pym/portage/tests/emerge/test_config_protect.py

diff --git a/pym/portage/dbapi/vartree.py b/pym/portage/dbapi/vartree.py
index e21135a..219ca16 100644
--- a/pym/portage/dbapi/vartree.py
+++ b/pym/portage/dbapi/vartree.py
@@ -4461,21 +4461,17 @@ class dblink(object):
 			# stat file once, test using S_* macros many times (faster that way)
 			mystat = os.lstat(mysrc)
 			mymode = mystat[stat.ST_MODE]
-			# handy variables; mydest is the target object on the live filesystems;
-			# mysrc is the source object in the temporary install dir
-			try:
-				mydstat = os.lstat(mydest)
-				mydmode = mydstat.st_mode
-			except OSError as e:
-				if e.errno != errno.ENOENT:
-					raise
-				del e
-				#dest file doesn't exist
-				mydstat = None
-				mydmode = None
+			mymd5 = None
+			myto = None
 
-			if stat.S_ISLNK(mymode):
-				# we are merging a symbolic link
+			if sys.hexversion >= 0x3030000:
+				mymtime = mystat.st_mtime_ns
+			else:
+				mymtime = mystat[stat.ST_MTIME]
+
+			if stat.S_ISREG(mymode):
+				mymd5 = perform_md5(mysrc, calc_prelink=calc_prelink)
+			elif stat.S_ISLNK(mymode):
 				# The file name of mysrc and the actual file that it points to
 				# will have earlier been forcefully converted to the 'merge'
 				# encoding if necessary, but the content of the symbolic link
@@ -4495,6 +4491,69 @@ class dblink(object):
 					os.unlink(mysrc)
 					os.symlink(myto, mysrc)
 
+				mymd5 = portage.checksum._new_md5(
+					_unicode_encode(myto)).hexdigest()
+
+			protected = False
+			if stat.S_ISLNK(mymode) or stat.S_ISREG(mymode):
+				protected = self.isprotected(mydest)
+
+				if stat.S_ISREG(mymode) and \
+					mystat.st_size == 0 and \
+					os.path.basename(mydest).startswith(".keep"):
+					protected = False
+
+			destmd5 = None
+			mydest_link = None
+			# handy variables; mydest is the target object on the live filesystems;
+			# mysrc is the source object in the temporary install dir
+			try:
+				mydstat = os.lstat(mydest)
+				mydmode = mydstat.st_mode
+				if protected:
+					if stat.S_ISLNK(mydmode):
+						# Read symlink target as bytes, in case the
+						# target path has a bad encoding.
+						mydest_link = _os.readlink(
+							_unicode_encode(mydest,
+							encoding=_encodings['merge'],
+							errors='strict'))
+						mydest_link = _unicode_decode(mydest_link,
+							encoding=_encodings['merge'],
+							errors='replace')
+
+						# For protection of symlinks, the md5
+						# of the link target path string is used
+						# for cfgfiledict (symlinks are
+						# protected since bug #485598).
+						destmd5 = portage.checksum._new_md5(
+							_unicode_encode(mydest_link)).hexdigest()
+
+					elif stat.S_ISREG(mydmode):
+						destmd5 = perform_md5(mydest,
+							calc_prelink=calc_prelink)
+			except (FileNotFound, OSError) as e:
+				if isinstance(e, OSError) and e.errno != errno.ENOENT:
+					raise
+				#dest file doesn't exist
+				mydstat = None
+				mydmode = None
+				mydest_link = None
+				destmd5 = None
+
+			moveme = True
+			if protected:
+				mydest, protected, moveme = self._protect(cfgfiledict,
+					protect_if_modified, mymd5, myto, mydest,
+					myrealdest, mydmode, destmd5, mydest_link)
+
+			zing = "!!!"
+			if not moveme:
+				# confmem rejected this update
+				zing = "---"
+
+			if stat.S_ISLNK(mymode):
+				# we are merging a symbolic link
 				# Pass in the symlink target in order to bypass the
 				# os.readlink() call inside abssymlink(), since that
 				# call is unsafe if the merge encoding is not ascii
@@ -4510,9 +4569,8 @@ class dblink(object):
 				# myrealto contains the path of the real file to which this symlink points.
 				# we can simply test for existence of this file to see if the target has been merged yet
 				myrealto = normalize_path(os.path.join(destroot, myabsto))
-				if mydmode!=None:
-					#destination exists
-					if stat.S_ISDIR(mydmode):
+				if mydmode is not None and stat.S_ISDIR(mydmode):
+					if not protected:
 						# we can't merge a symlink over a directory
 						newdest = self._new_backup_path(mydest)
 						msg = []
@@ -4525,22 +4583,6 @@ class dblink(object):
 						self._eerror("preinst", msg)
 						mydest = newdest
 
-					elif not stat.S_ISLNK(mydmode):
-						if os.path.exists(mysrc) and stat.S_ISDIR(os.stat(mysrc)[stat.ST_MODE]):
-							# Kill file blocking installation of symlink to dir #71787
-							pass
-						elif self.isprotected(mydest):
-							# Use md5 of the target in ${D} if it exists...
-							try:
-								newmd5 = perform_md5(join(srcroot, myabsto))
-							except FileNotFound:
-								# Maybe the target is merged already.
-								try:
-									newmd5 = perform_md5(myrealto)
-								except FileNotFound:
-									newmd5 = None
-							mydest = new_protect_filename(mydest, newmd5=newmd5)
-
 				# if secondhand is None it means we're operating in "force" mode and should not create a second hand.
 				if (secondhand != None) and (not os.path.exists(myrealto)):
 					# either the target directory doesn't exist yet or the target file doesn't exist -- or
@@ -4549,9 +4591,11 @@ class dblink(object):
 					secondhand.append(mysrc[len(srcroot):])
 					continue
 				# unlinking no longer necessary; "movefile" will overwrite symlinks atomically and correctly
-				mymtime = movefile(mysrc, mydest, newmtime=thismtime,
-					sstat=mystat, mysettings=self.settings,
-					encoding=_encodings['merge'])
+				if moveme:
+					zing = ">>>"
+					mymtime = movefile(mysrc, mydest, newmtime=thismtime,
+						sstat=mystat, mysettings=self.settings,
+						encoding=_encodings['merge'])
 
 				try:
 					self._merged_path(mydest, os.lstat(mydest))
@@ -4567,7 +4611,7 @@ class dblink(object):
 							[_("QA Notice: Symbolic link /%s points to /%s which does not exist.")
 							% (relative_path, myabsto)])
 
-					showMessage(">>> %s -> %s\n" % (mydest, myto))
+					showMessage("%s %s -> %s\n" % (zing, mydest, myto))
 					if sys.hexversion >= 0x3030000:
 						outfile.write("sym "+myrealdest+" -> "+myto+" "+str(mymtime // 1000000000)+"\n")
 					else:
@@ -4589,7 +4633,8 @@ class dblink(object):
 						if dflags != 0:
 							bsd_chflags.lchflags(mydest, 0)
 
-					if not os.access(mydest, os.W_OK):
+					if not stat.S_ISLNK(mydmode) and \
+						not os.access(mydest, os.W_OK):
 						pkgstuff = pkgsplit(self.pkg)
 						writemsg(_("\n!!! Cannot write to '%s'.\n") % mydest, noiselevel=-1)
 						writemsg(_("!!! Please check permissions and directories for broken symlinks.\n"))
@@ -4678,14 +4723,8 @@ class dblink(object):
 
 			elif stat.S_ISREG(mymode):
 				# we are merging a regular file
-				mymd5 = perform_md5(mysrc, calc_prelink=calc_prelink)
-				# calculate config file protection stuff
-				mydestdir = os.path.dirname(mydest)
-				moveme = 1
-				zing = "!!!"
-				mymtime = None
-				protected = self.isprotected(mydest)
-				if mydmode is not None and stat.S_ISDIR(mydmode):
+				if not protected and \
+					mydmode is not None and stat.S_ISDIR(mydmode):
 						# install of destination is blocked by an existing directory with the same name
 						newdest = self._new_backup_path(mydest)
 						msg = []
@@ -4698,73 +4737,6 @@ class dblink(object):
 						self._eerror("preinst", msg)
 						mydest = newdest
 
-				elif mydmode is None or stat.S_ISREG(mydmode) or \
-					(stat.S_ISLNK(mydmode) and os.path.exists(mydest)
-					and stat.S_ISREG(os.stat(mydest)[stat.ST_MODE])):
-						# install of destination is blocked by an existing regular file,
-						# or by a symlink to an existing regular file;
-						# now, config file management may come into play.
-						# we only need to tweak mydest if cfg file management is in play.
-						destmd5 = None
-						if protected and mydmode is not None:
-							destmd5 = perform_md5(mydest, calc_prelink=calc_prelink)
-							if protect_if_modified:
-								contents_key = \
-									self._installed_instance._match_contents(myrealdest)
-								if contents_key:
-									inst_info = self._installed_instance.getcontents()[contents_key]
-									if inst_info[0] == "obj" and inst_info[2] == destmd5:
-										protected = False
-
-						if protected:
-							# we have a protection path; enable config file management.
-							cfgprot = 0
-							cfgprot_force = False
-							if mydmode is None:
-								if self._installed_instance is not None and \
-									self._installed_instance._match_contents(
-									myrealdest) is not False:
-									# If the file doesn't exist, then it may
-									# have been deleted or renamed by the
-									# admin. Therefore, force the file to be
-									# merged with a ._cfg name, so that the
-									# admin will be prompted for this update
-									# (see bug #523684).
-									cfgprot_force = True
-									moveme = True
-									cfgprot = True
-							elif mymd5 == destmd5:
-								#file already in place; simply update mtimes of destination
-								moveme = 1
-							else:
-								if mymd5 == cfgfiledict.get(myrealdest, [None])[0]:
-									""" An identical update has previously been
-									merged.  Skip it unless the user has chosen
-									--noconfmem."""
-									moveme = cfgfiledict["IGNORE"]
-									cfgprot = cfgfiledict["IGNORE"]
-									if not moveme:
-										zing = "---"
-										if sys.hexversion >= 0x3030000:
-											mymtime = mystat.st_mtime_ns
-										else:
-											mymtime = mystat[stat.ST_MTIME]
-								else:
-									moveme = 1
-									cfgprot = 1
-							if moveme:
-								# Merging a new file, so update confmem.
-								cfgfiledict[myrealdest] = [mymd5]
-							elif destmd5 == cfgfiledict.get(myrealdest, [None])[0]:
-								"""A previously remembered update has been
-								accepted, so it is removed from confmem."""
-								del cfgfiledict[myrealdest]
-
-							if cfgprot:
-								mydest = new_protect_filename(mydest,
-									newmd5=mymd5,
-									force=cfgprot_force)
-
 				# whether config protection or not, we merge the new file the
 				# same way.  Unless moveme=0 (blocking directory)
 				if moveme:
@@ -4820,6 +4792,63 @@ class dblink(object):
 					outfile.write("dev %s\n" % myrealdest)
 				showMessage(zing + " " + mydest + "\n")
 
+	def _protect(self, cfgfiledict, protect_if_modified, mymd5, myto,
+		mydest, myrealdest, mydmode, destmd5, mydest_link):
+
+		moveme = True
+		protected = True
+		force = False
+		k = False
+		if self._installed_instance is not None:
+			k = self._installed_instance._match_contents(myrealdest)
+		if k is not False:
+			if mydmode is None:
+				# If the file doesn't exist, then it may
+				# have been deleted or renamed by the
+				# admin. Therefore, force the file to be
+				# merged with a ._cfg name, so that the
+				# admin will be prompted for this update
+				# (see bug #523684).
+				force = True
+
+			elif protect_if_modified:
+				data = self._installed_instance.getcontents()[k]
+				if data[0] == "obj" and data[2] == destmd5:
+					protected = False
+				elif data[0] == "sym" and data[2] == mydest_link:
+					protected = False
+
+		if protected and mydmode is not None:
+			# we have a protection path; enable config file management.
+			if mymd5 != destmd5 and \
+				mymd5 == cfgfiledict.get(myrealdest, [None])[0]:
+				# An identical update has previously been
+				# merged.  Skip it unless the user has chosen
+				# --noconfmem.
+				moveme = protected = bool(cfgfiledict["IGNORE"])
+
+			if protected and \
+				(mydest_link is not None or myto is not None) and \
+				mydest_link != myto:
+				# If either one is a symlink, and they are not
+				# identical symlinks, then force config protection.
+				force = True
+
+			if moveme:
+				# Merging a new file, so update confmem.
+				cfgfiledict[myrealdest] = [mymd5]
+			elif destmd5 == cfgfiledict.get(myrealdest, [None])[0]:
+				# A previously remembered update has been
+				# accepted, so it is removed from confmem.
+				del cfgfiledict[myrealdest]
+
+		if protected and moveme:
+			mydest = new_protect_filename(mydest,
+				newmd5=(mydest_link or mymd5),
+				force=force)
+
+		return mydest, protected, moveme
+
 	def _merged_path(self, path, lstatobj, exists=True):
 		previous_path = self._device_path_map.get(lstatobj.st_dev)
 		if previous_path is None or previous_path is False or \
diff --git a/pym/portage/tests/emerge/test_config_protect.py b/pym/portage/tests/emerge/test_config_protect.py
new file mode 100644
index 0000000..5d7d8e9
--- /dev/null
+++ b/pym/portage/tests/emerge/test_config_protect.py
@@ -0,0 +1,292 @@
+# Copyright 2014 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+
+from __future__ import unicode_literals
+
+import io
+from functools import partial
+import shutil
+import stat
+import subprocess
+import sys
+import time
+
+import portage
+from portage import os
+from portage import _encodings, _unicode_decode
+from portage.const import BASH_BINARY, PORTAGE_PYM_PATH
+from portage.process import find_binary
+from portage.tests import TestCase
+from portage.tests.resolver.ResolverPlayground import ResolverPlayground
+from portage.util import (ensure_dirs, find_updated_config_files,
+	shlex_split)
+
+class ConfigProtectTestCase(TestCase):
+
+	def testConfigProtect(self):
+		"""
+		Demonstrates many different scenarios. For example:
+
+		 * regular file replaces regular file
+		 * regular file replaces symlink
+		 * regular file replaces directory
+		 * symlink replaces symlink
+		 * symlink replaces regular file
+		 * symlink replaces directory
+		 * directory replaces regular file
+		 * directory replaces symlink
+		"""
+
+		debug = False
+
+		content_A_1 = """
+S="${WORKDIR}"
+
+src_install() {
+	insinto /etc/A
+	keepdir /etc/A/dir_a
+	keepdir /etc/A/symlink_replaces_dir
+	keepdir /etc/A/regular_replaces_dir
+	echo regular_a_1 > "${T}"/regular_a
+	doins "${T}"/regular_a
+	echo regular_b_1 > "${T}"/regular_b
+	doins "${T}"/regular_b
+	dosym regular_a /etc/A/regular_replaces_symlink
+	dosym regular_b /etc/A/symlink_replaces_symlink
+	echo regular_replaces_regular_1 > \
+		"${T}"/regular_replaces_regular
+	doins "${T}"/regular_replaces_regular
+	echo symlink_replaces_regular > \
+		"${T}"/symlink_replaces_regular
+	doins "${T}"/symlink_replaces_regular
+}
+
+"""
+
+		content_A_2 = """
+S="${WORKDIR}"
+
+src_install() {
+	insinto /etc/A
+	keepdir /etc/A/dir_a
+	dosym dir_a /etc/A/symlink_replaces_dir
+	echo regular_replaces_dir > "${T}"/regular_replaces_dir
+	doins "${T}"/regular_replaces_dir
+	echo regular_a_2 > "${T}"/regular_a
+	doins "${T}"/regular_a
+	echo regular_b_2 > "${T}"/regular_b
+	doins "${T}"/regular_b
+	echo regular_replaces_symlink > \
+		"${T}"/regular_replaces_symlink
+	doins "${T}"/regular_replaces_symlink
+	dosym regular_b /etc/A/symlink_replaces_symlink
+	echo regular_replaces_regular_2 > \
+		"${T}"/regular_replaces_regular
+	doins "${T}"/regular_replaces_regular
+	dosym regular_a /etc/A/symlink_replaces_regular
+}
+
+"""
+
+		ebuilds = {
+			"dev-libs/A-1": {
+				"EAPI" : "5",
+				"IUSE" : "+flag",
+				"KEYWORDS": "x86",
+				"LICENSE": "GPL-2",
+				"MISC_CONTENT": content_A_1,
+			},
+			"dev-libs/A-2": {
+				"EAPI" : "5",
+				"IUSE" : "+flag",
+				"KEYWORDS": "x86",
+				"LICENSE": "GPL-2",
+				"MISC_CONTENT": content_A_2,
+			},
+		}
+
+		playground = ResolverPlayground(
+			ebuilds=ebuilds, debug=debug)
+		settings = playground.settings
+		eprefix = settings["EPREFIX"]
+		eroot = settings["EROOT"]
+		var_cache_edb = os.path.join(eprefix, "var", "cache", "edb")
+
+		portage_python = portage._python_interpreter
+		dispatch_conf_cmd = (portage_python, "-b", "-Wd",
+			os.path.join(self.sbindir, "dispatch-conf"))
+		emerge_cmd = (portage_python, "-b", "-Wd",
+			os.path.join(self.bindir, "emerge"))
+		etc_update_cmd = (BASH_BINARY,
+			os.path.join(self.sbindir, "etc-update"))
+		etc_update_auto = etc_update_cmd + ("--automode", "-5",)
+
+		config_protect = "/etc"
+
+		def modify_files(dir_path):
+			for name in os.listdir(dir_path):
+				path = os.path.join(dir_path, name)
+				st = os.lstat(path)
+				if stat.S_ISREG(st.st_mode):
+					with io.open(path, mode='a',
+						encoding=_encodings["stdio"]) as f:
+						f.write("modified at %d\n" % time.time())
+				elif stat.S_ISLNK(st.st_mode):
+					old_dest = os.readlink(path)
+					os.unlink(path)
+					os.symlink(old_dest +
+						" modified at %d" % time.time(), path)
+
+		def updated_config_files(count):
+			self.assertEqual(count,
+				sum(len(x[1]) for x in find_updated_config_files(eroot,
+				shlex_split(config_protect))))
+
+		test_commands = (
+			etc_update_cmd,
+			dispatch_conf_cmd,
+			emerge_cmd + ("-1", "=dev-libs/A-1"),
+			partial(updated_config_files, 0),
+			emerge_cmd + ("-1", "=dev-libs/A-2"),
+			partial(updated_config_files, 2),
+			etc_update_auto,
+			partial(updated_config_files, 0),
+			emerge_cmd + ("-1", "=dev-libs/A-2"),
+			partial(updated_config_files, 0),
+			# Test bug #523684, where a file renamed or removed by the
+			# admin forces replacement files to be merged with config
+			# protection.
+			partial(shutil.rmtree,
+				os.path.join(eprefix, "etc", "A")),
+			emerge_cmd + ("-1", "=dev-libs/A-2"),
+			partial(updated_config_files, 8),
+			etc_update_auto,
+			partial(updated_config_files, 0),
+			# Modify some config files, and verify that it triggers
+			# config protection.
+			partial(modify_files,
+				os.path.join(eroot, "etc", "A")),
+			emerge_cmd + ("-1", "=dev-libs/A-2"),
+			partial(updated_config_files, 6),
+			etc_update_auto,
+			partial(updated_config_files, 0),
+			# Modify some config files, downgrade to A-1, and verify
+			# that config protection works properly when the file
+			# types are changing.
+			partial(modify_files,
+				os.path.join(eroot, "etc", "A")),
+			emerge_cmd + ("-1", "--noconfmem", "=dev-libs/A-1"),
+			partial(updated_config_files, 6),
+			etc_update_auto,
+			partial(updated_config_files, 0),
+		)
+
+		distdir = playground.distdir
+		fake_bin = os.path.join(eprefix, "bin")
+		portage_tmpdir = os.path.join(eprefix, "var", "tmp", "portage")
+
+		path =  os.environ.get("PATH")
+		if path is not None and not path.strip():
+			path = None
+		if path is None:
+			path = ""
+		else:
+			path = ":" + path
+		path = fake_bin + path
+
+		pythonpath =  os.environ.get("PYTHONPATH")
+		if pythonpath is not None and not pythonpath.strip():
+			pythonpath = None
+		if pythonpath is not None and \
+			pythonpath.split(":")[0] == PORTAGE_PYM_PATH:
+			pass
+		else:
+			if pythonpath is None:
+				pythonpath = ""
+			else:
+				pythonpath = ":" + pythonpath
+			pythonpath = PORTAGE_PYM_PATH + pythonpath
+
+		env = {
+			"PORTAGE_OVERRIDE_EPREFIX" : eprefix,
+			"CLEAN_DELAY" : "0",
+			"CONFIG_PROTECT": config_protect,
+			"DISTDIR" : distdir,
+			"EMERGE_DEFAULT_OPTS": "-v",
+			"EMERGE_WARNING_DELAY" : "0",
+			"INFODIR" : "",
+			"INFOPATH" : "",
+			"PATH" : path,
+			"PORTAGE_INST_GID" : str(portage.data.portage_gid),
+			"PORTAGE_INST_UID" : str(portage.data.portage_uid),
+			"PORTAGE_PYTHON" : portage_python,
+			"PORTAGE_REPOSITORIES" : settings.repositories.config_string(),
+			"PORTAGE_TMPDIR" : portage_tmpdir,
+			"PYTHONPATH" : pythonpath,
+			"__PORTAGE_TEST_PATH_OVERRIDE" : fake_bin,
+		}
+
+		if "__PORTAGE_TEST_HARDLINK_LOCKS" in os.environ:
+			env["__PORTAGE_TEST_HARDLINK_LOCKS"] = \
+				os.environ["__PORTAGE_TEST_HARDLINK_LOCKS"]
+
+		dirs = [distdir, fake_bin, portage_tmpdir,
+			var_cache_edb]
+		etc_symlinks = ("dispatch-conf.conf", "etc-update.conf")
+		# Override things that may be unavailable, or may have portability
+		# issues when running tests in exotic environments.
+		#   prepstrip - bug #447810 (bash read builtin EINTR problem)
+		true_symlinks = ["prepstrip", "scanelf"]
+		true_binary = find_binary("true")
+		self.assertEqual(true_binary is None, False,
+			"true command not found")
+		try:
+			for d in dirs:
+				ensure_dirs(d)
+			for x in true_symlinks:
+				os.symlink(true_binary, os.path.join(fake_bin, x))
+			for x in etc_symlinks:
+				os.symlink(os.path.join(self.cnf_etc_path, x),
+					os.path.join(eprefix, "etc", x))
+			with open(os.path.join(var_cache_edb, "counter"), 'wb') as f:
+				f.write(b"100")
+
+			if debug:
+				# The subprocess inherits both stdout and stderr, for
+				# debugging purposes.
+				stdout = None
+			else:
+				# The subprocess inherits stderr so that any warnings
+				# triggered by python -Wd will be visible.
+				stdout = subprocess.PIPE
+
+			for args in test_commands:
+
+				if hasattr(args, '__call__'):
+					args()
+					continue
+
+				if isinstance(args[0], dict):
+					local_env = env.copy()
+					local_env.update(args[0])
+					args = args[1:]
+				else:
+					local_env = env
+
+				proc = subprocess.Popen(args,
+					env=local_env, stdout=stdout)
+
+				if debug:
+					proc.wait()
+				else:
+					output = proc.stdout.readlines()
+					proc.wait()
+					proc.stdout.close()
+					if proc.returncode != os.EX_OK:
+						for line in output:
+							sys.stderr.write(_unicode_decode(line))
+
+				self.assertEqual(os.EX_OK, proc.returncode,
+					"emerge failed with args %s" % (args,))
+		finally:
+			playground.cleanup()
diff --git a/pym/portage/util/__init__.py b/pym/portage/util/__init__.py
index fe79942..ad3a351 100644
--- a/pym/portage/util/__init__.py
+++ b/pym/portage/util/__init__.py
@@ -1676,13 +1676,36 @@ def new_protect_filename(mydest, newmd5=None, force=False):
 	old_pfile = normalize_path(os.path.join(real_dirname, last_pfile))
 	if last_pfile and newmd5:
 		try:
-			last_pfile_md5 = portage.checksum._perform_md5_merge(old_pfile)
-		except FileNotFound:
-			# The file suddenly disappeared or it's a broken symlink.
-			pass
+			old_pfile_st = _os_merge.lstat(old_pfile)
+		except OSError as e:
+			if e.errno != errno.ENOENT:
+				raise
 		else:
-			if last_pfile_md5 == newmd5:
-				return old_pfile
+			if stat.S_ISLNK(old_pfile_st.st_mode):
+				try:
+					# Read symlink target as bytes, in case the
+					# target path has a bad encoding.
+					pfile_link = _os.readlink(_unicode_encode(old_pfile,
+						encoding=_encodings['merge'], errors='strict'))
+				except OSError:
+					if e.errno != errno.ENOENT:
+						raise
+				else:
+					pfile_link = _unicode_decode(
+						encoding=_encodings['merge'], errors='replace')
+					if pfile_link == newmd5:
+						return old_pfile
+			else:
+				try:
+					last_pfile_md5 = \
+						portage.checksum._perform_md5_merge(old_pfile)
+				except FileNotFound:
+					# The file suddenly disappeared or it's a
+					# broken symlink.
+					pass
+				else:
+					if last_pfile_md5 == newmd5:
+						return old_pfile
 	return new_pfile
 
 def find_updated_config_files(target_root, config_protect):
-- 
2.0.4



  parent reply	other threads:[~2014-10-26 11:12 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2014-10-26 11:12 [gentoo-portage-dev] [PATCH 1/3] etc-update: symlink support for bug #485598 zmedico
2014-10-26 11:12 ` [gentoo-portage-dev] [PATCH 2/3] dispatch-conf: " zmedico
2014-10-26 11:12 ` zmedico [this message]
2014-10-27  8:08   ` [gentoo-portage-dev] [PATCH 3/3] CONFIG_PROTECT: protect symlinks, " Alexander Berntsen
2014-10-27  9:07     ` Zac Medico
2014-10-27 20:35     ` Zac Medico
2014-11-03  3:54       ` Brian Dolbec
2014-10-27 22:57 ` [gentoo-portage-dev] [PATCH 1/3] etc-update: symlink support for " Zac Medico
2014-10-27 23:04 ` [gentoo-portage-dev] [PATCH 2/3] dispatch-conf: " Zac Medico
2014-10-31 13:34 ` [gentoo-portage-dev] [PATCH] " Zac Medico

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=1414321936-22851-3-git-send-email-zmedico@gentoo.org \
    --to=zmedico@gentoo.org \
    --cc=gentoo-portage-dev@lists.gentoo.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox