install_mask: remove masked symlinks to directories (bug 678462)

os.walk() categorizes symlinks to directories as directories so they
were being ignored by INSTALL_MASK. This change calls os.scandir()
instead which efficiently provides more control.

BUG=chromium:1069710
TEST=runtests; build_image

Change-Id: If26aff91bf8b1c58e616f6c533aa793785c561cb
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/portage_tool/+/2872928
Tested-by: Jeff Chase <jnchase@google.com>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
Commit-Queue: Jeff Chase <jnchase@google.com>
diff --git a/lib/portage/tests/util/test_install_mask.py b/lib/portage/tests/util/test_install_mask.py
index 6a29db7..665a8ed 100644
--- a/lib/portage/tests/util/test_install_mask.py
+++ b/lib/portage/tests/util/test_install_mask.py
@@ -1,8 +1,11 @@
 # Copyright 2018 Gentoo Foundation
 # Distributed under the terms of the GNU General Public License v2
 
+import tempfile
+from portage import os
+from portage import shutil
 from portage.tests import TestCase
-from portage.util.install_mask import InstallMask
+from portage.util.install_mask import InstallMask, install_mask_dir
 
 
 class InstallMaskTestCase(TestCase):
@@ -163,3 +166,24 @@
 				self.assertEqual(install_mask.match(path), expected,
 					'unexpected match result for "{}" with path {}'.\
 					format(install_mask_str, path))
+
+	def testSymlinkDir(self):
+		"""
+		Test that masked symlinks to directories are removed.
+		"""
+		tmp_dir = tempfile.mkdtemp()
+		try:
+			base_dir = os.path.join(tmp_dir, 'foo')
+			target_dir = os.path.join(tmp_dir, 'foo', 'bar')
+			link_name = os.path.join(tmp_dir, 'foo', 'baz')
+			os.mkdir(base_dir)
+			os.mkdir(target_dir)
+			os.symlink(target_dir, link_name)
+			install_mask = InstallMask('/foo/')
+			install_mask_dir(tmp_dir, install_mask)
+			self.assertFalse(os.path.lexists(link_name),
+				'failed to remove {}'.format(link_name))
+			self.assertFalse(os.path.lexists(base_dir),
+				'failed to remove {}'.format(base_dir))
+		finally:
+			shutil.rmtree(tmp_dir)
diff --git a/lib/portage/util/install_mask.py b/lib/portage/util/install_mask.py
index 0013eff..3a59b15 100644
--- a/lib/portage/util/install_mask.py
+++ b/lib/portage/util/install_mask.py
@@ -172,22 +172,26 @@
 	dir_stack = []
 
 	# Remove masked files.
-	for parent, dirs, files in os.walk(base_dir, onerror=onerror):
+	todo = [base_dir]
+	while todo:
+		parent = todo.pop()
 		try:
 			parent = _unicode_decode(parent, errors='strict')
 		except UnicodeDecodeError:
 			continue
+
 		dir_stack.append(parent)
-		for fname in files:
+		for entry in os.scandir(parent):
 			try:
-				fname = _unicode_decode(fname, errors='strict')
+				abs_path = _unicode_decode(entry.path, errors='strict')
 			except UnicodeDecodeError:
 				continue
-			abs_path = os.path.join(parent, fname)
-			relative_path = abs_path[base_dir_len:]
-			if install_mask.match(relative_path):
+
+			if entry.is_dir(follow_symlinks=False):
+				todo.append(entry.path)
+			elif install_mask.match(abs_path[base_dir_len:]):
 				try:
-					os.unlink(abs_path)
+					os.unlink(entry.path)
 				except OSError as e:
 					onerror(e)