16389464 multiple whitespaces in action path can cause package metadata errors
authorShawn Walker <shawn.walker@oracle.com>
Thu, 28 Feb 2013 16:50:44 -0800
changeset 2876 fd422f3cc04f
parent 2875 09e276ba70c6
child 2878 bb1d23c3e089
16389464 multiple whitespaces in action path can cause package metadata errors
src/modules/manifest.py
src/tests/api/t_manifest.py
src/tests/cli/t_pkg_install.py
--- a/src/modules/manifest.py	Mon Mar 11 15:15:21 2013 -0700
+++ b/src/modules/manifest.py	Thu Feb 28 16:50:44 2013 -0800
@@ -41,6 +41,7 @@
 
 from pkg.misc import EmptyDict, EmptyI, expanddirs, PKG_FILE_MODE, PKG_DIR_MODE
 from pkg.actions.attribute import AttributeAction
+from pkg.actions.directory import DirectoryAction
 
 class ManifestDifference(
     namedtuple("ManifestDifference", "added changed removed")):
@@ -328,9 +329,8 @@
                 dirs = self._actions_to_dict(gen_references)
                 for d in dirs:
                         for v in dirs[d]:
-                                yield "dir path=%s %s\n" % \
-                                    (d, " ".join("%s=%s" % t \
-                                    for t in v.iteritems()))
+                                a = DirectoryAction(path=d, **v)
+                                yield str(a) + "\n"
 
         def _gen_mediators_to_str(self):
                 """Generate contents of mediatorcache file containing all
@@ -1148,28 +1148,39 @@
                 """
 
                 mpath = self.__cache_path(name)
-                if not os.path.exists(mpath):
-                        # no cached copy
-                        if not self.loaded:
-                                # need to load from disk
-                                self.__load()
-                        assert self.loaded
-                        return
+                if os.path.exists(mpath):
+                        # we have cached copy on disk; use it
+                        try:
+                                with open(mpath, "rb") as f:
+                                        self._cache[name] = [
+                                            a for a in
+                                            (
+                                                actions.fromstr(s.rstrip())
+                                                for s in f
+                                            )
+                                            if not self.excludes or
+                                                a.include_this(self.excludes)
+                                        ]
+                                return
+                        except EnvironmentError, e:
+                                raise apx._convert_error(e)
+                        except actions.ActionError, e:
+                                # Cache file is malformed; hopefully due to bugs
+                                # that have been resolved (as opposed to actual
+                                # corruption).  Assume we should just ignore the
+                                # cache and load action data.
+                                try:
+                                        self.clear_cache(self.__cache_root)
+                                except Exception, e:
+                                        # Ignore errors encountered during cache
+                                        # dump for this specific case.
+                                        pass
 
-                # we have cached copy on disk; use it
-                try:
-                        with open(mpath, "rb") as f:
-                                self._cache[name] = [
-                                    a for a in
-                                    (
-                                        actions.fromstr(s.rstrip())
-                                        for s in f
-                                    )
-                                    if not self.excludes or
-                                        a.include_this(self.excludes)
-                                ]
-                except EnvironmentError, e:
-                        raise apx._convert_error(e)
+                # no cached copy
+                if not self.loaded:
+                        # need to load from disk
+                        self.__load()
+                assert self.loaded
 
         def get_directories(self, excludes):
                 """ return a list of directories implicitly or explicitly
--- a/src/tests/api/t_manifest.py	Mon Mar 11 15:15:21 2013 -0700
+++ b/src/tests/api/t_manifest.py	Thu Feb 28 16:50:44 2013 -0800
@@ -21,7 +21,7 @@
 # CDDL HEADER END
 #
 
-# Copyright (c) 2008, 2012, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved.
 
 import unittest
 import tempfile
@@ -79,7 +79,9 @@
 set com.sun,test=false
 depend type=require fmri=pkg:/library/libc
 file fff555ff9 mode=0555 owner=sch group=staff path=/usr/bin/i386/sort isa=i386
-dir owner=root path=usr/bin group=bin mode=0755
+dir owner=root path=usr/bin group=bin mode=0755 variant.arch=i386 variant.arch=sparc
+dir owner=root path="opt/dir with spaces in value" group=bin mode=0755
+dir owner=root path="opt/dir with whitespaces	in value" group=bin mode=0755
 link path=usr/lib/amd64/libjpeg.so \
 target=libjpeg.so.62.0.0
 hardlink path=usr/bin/amd64/rksh93 target=ksh93 variant.opensolaris.zone=global
@@ -275,14 +277,14 @@
                 #
                 # Expect to see something -> None differences
                 #
-                self.assertEqual(len(diffs), 7)
+                self.assertEqual(len(diffs), 9)
                 for d in diffs:
                         self.assertEqual(type(d[1]), types.NoneType)
 
                 #
                 # Expect to see None -> something differences
                 #
-                self.assertEqual(len(diffs2), 7)
+                self.assertEqual(len(diffs2), 9)
                 for d in diffs2:
                         self.assertEqual(type(d[0]), types.NoneType)
                         self.assertNotEqual(type(d[1]), types.NoneType)
@@ -378,9 +380,18 @@
 
 class TestFactoredManifest(pkg5unittest.Pkg5TestCase):
 
+        foo_content = """\
+set name=pkg.fmri value=pkg:/[email protected]
+dir owner=root path=usr/bin group=bin mode=0755 variant.arch=i386
+dir owner=root path="opt/dir with spaces in value" group=bin mode=0755
+dir owner=root path="opt/dir with whitespaces	in value" group=bin mode=0755 variant.debug.osnet=true
+"""
+
         def setUp(self):
                 pkg5unittest.Pkg5TestCase.setUp(self)
                 self.cache_dir = tempfile.mkdtemp()
+                self.foo_content_p5m = self.make_misc_files(
+                    { "foo_content.p5m": self.foo_content })[0]
 
         def test_cached_gen_actions_by_type(self):
                 """Test that when a factored manifest generates actions by type
@@ -400,6 +411,77 @@
                 m1.exclude_content([v.allow_action, lambda x: True])
                 self.assertEqual(len(list(m1.gen_actions_by_type("dir"))), 1)
 
+        def test_get_directories(self):
+                """Verifies that get_directories() works as expected."""
+
+                v = variant.Variants({ "variant.arch": "sparc" })
+                excludes = [v.allow_action, lambda x: True]
+
+                m1 = manifest.FactoredManifest("[email protected]", self.cache_dir,
+                    pathname=self.foo_content_p5m)
+
+                all_expected = [
+                    "opt/dir with spaces in value",
+                    "opt",
+                    "usr/bin",
+                    "opt/dir with whitespaces	in value",
+                    "usr"
+                ]
+
+                var_expected = [
+                    "opt/dir with spaces in value",
+                    "opt"
+                ]
+
+                def do_get_dirs():
+                        actual = m1.get_directories([])
+                        self.assertEqualDiff(all_expected, actual)
+
+                        actual = m1.get_directories(excludes)
+                        self.assertEqualDiff(var_expected, actual)
+
+                # Verify get_directories works for initial load.
+                do_get_dirs()
+
+                # Now repeat experiment using "cached" FactoredManifest.
+                cfile_path = os.path.join(self.cache_dir, "manifest.dircache")
+                self.assert_(os.path.isfile(cfile_path))
+                m1 = manifest.FactoredManifest("[email protected]", self.cache_dir,
+                    pathname=self.foo_content_p5m)
+
+                do_get_dirs()
+
+                # Now rewrite the dircache so that it contains actions with
+                # paths that are not properly quoted to simulate older, broken
+                # behaviour and verify that the cache will be removed and that
+                # get_directories() still works as expected.
+                m1 = manifest.FactoredManifest("[email protected]", self.cache_dir,
+                    pathname=self.foo_content_p5m)
+
+                with open(cfile_path, "wb") as f:
+                        for a in m1.gen_actions_by_type("dir"):
+                                f.write(
+                                    "dir path=%s %s\n" % (a.attrs["path"],
+                                        " ".join(
+                                            "%s=%s" % (attr, a.attrs[attr])
+                                            for attr in itertools.chain(
+                                                *a.get_varcet_keys())
+                                        )
+                                ))
+
+                # Repeat tests again.
+                do_get_dirs()
+
+                # Verify cache file was removed (presumably because we
+                # detected it was malformed).
+                self.assert_(not os.path.exists(cfile_path))
+
+                # Repeat tests again, verifying cache file is recreated.
+                m1 = manifest.FactoredManifest("[email protected]", self.cache_dir,
+                    pathname=self.foo_content_p5m)
+                do_get_dirs()
+                self.assert_(os.path.isfile(cfile_path))
+
 
 if __name__ == "__main__":
         unittest.main()
--- a/src/tests/cli/t_pkg_install.py	Mon Mar 11 15:15:21 2013 -0700
+++ b/src/tests/cli/t_pkg_install.py	Thu Feb 28 16:50:44 2013 -0800
@@ -181,6 +181,17 @@
             add file tmp/cat mode=0555 owner=root group=bin path=/bin/cat
             close """
 
+        fuzzy = """
+            open [email protected],5.11-0
+            add dir mode=0755 owner=root group=bin path="opt/dir with white\tspace"
+            add file tmp/cat mode=0644 owner=root group=bin path="opt/dir with white\tspace/cat in a hat"
+            close
+            open [email protected],5.11-0
+            add dir mode=0755 owner=root group=bin path="opt/dir with white\tspace"
+            add file tmp/cat mode=0644 owner=root group=bin path="opt/dir with white\tspace/cat in a hat"
+            add link path=etc/cat_link target="../opt/dir with white\tspace/cat in a hat"
+            close """
+
         misc_files = [ "tmp/libc.so.1", "tmp/cat", "tmp/baz" ]
 
         def setUp(self):
@@ -748,6 +759,29 @@
                 afobj.close()
                 self.pkg("install a16189", exit=1)
 
+        def test_install_fuzz(self):
+                """Verify that packages delivering files with whitespace in
+                their paths can be installed, updated, and uninstalled."""
+
+                self.pkgsend_bulk(self.rurl, self.fuzzy)
+                self.image_create(self.rurl)
+
+                self.pkg("install fuzzy@1")
+                self.pkg("verify -v")
+                self.pkg("update -vvv fuzzy@2")
+                self.pkg("verify -v")
+
+                for name in (
+                    "opt/dir with white\tspace/cat in a hat",
+                    "etc/cat_link",
+                ):
+                        self.debug("fname: %s" % name)
+                        self.assert_(os.path.exists(os.path.join(self.get_img_path(),
+                            name)))
+
+                self.pkg("uninstall -vvv fuzzy")
+
+
 class TestPkgInstallApache(pkg5unittest.ApacheDepotTestCase):
 
         # Only start/stop the depot once (instead of for every test)