dirnodes: fix normalization hole where childnames in directories created by nodemaker.create_mutable/immutable_directory would not be normalized. Add a test that we normalize names coming out of a directory.

This commit is contained in:
david-sarah
2010-06-17 17:02:49 -07:00
parent 718870a796
commit a9fe3792de
3 changed files with 67 additions and 38 deletions

View File

@ -163,7 +163,7 @@ class Adder:
precondition(IFilesystemNode.providedBy(child), child) precondition(IFilesystemNode.providedBy(child), child)
# Strictly speaking this is redundant because we would raise the # Strictly speaking this is redundant because we would raise the
# error again in pack_children. # error again in _pack_normalized_children.
child.raise_error() child.raise_error()
metadata = None metadata = None
@ -199,9 +199,21 @@ def _encrypt_rw_uri(filenode, rw_uri):
# The MAC is not checked by readers in Tahoe >= 1.3.0, but we still # The MAC is not checked by readers in Tahoe >= 1.3.0, but we still
# produce it for the sake of older readers. # produce it for the sake of older readers.
def pack_children(filenode, children, deep_immutable=False):
def pack_children(filenode, childrenx, deep_immutable=False):
# initial_children must have metadata (i.e. {} instead of None)
children = {}
for (namex, (node, metadata)) in childrenx.iteritems():
precondition(isinstance(metadata, dict),
"directory creation requires metadata to be a dict, not None", metadata)
children[normalize(namex)] = (node, metadata)
return _pack_normalized_children(filenode, children, deep_immutable=deep_immutable)
def _pack_normalized_children(filenode, children, deep_immutable=False):
"""Take a dict that maps: """Take a dict that maps:
children[unicode_name] = (IFileSystemNode, metadata_dict) children[unicode_nfc_name] = (IFileSystemNode, metadata_dict)
and pack it into a single string, for use as the contents of the backing and pack it into a single string, for use as the contents of the backing
file. This is the same format as is returned by _unpack_contents. I also file. This is the same format as is returned by _unpack_contents. I also
accept an AuxValueDict, in which case I'll use the auxilliary cached data accept an AuxValueDict, in which case I'll use the auxilliary cached data
@ -368,7 +380,7 @@ class DirectoryNode:
def _pack_contents(self, children): def _pack_contents(self, children):
# expects children in the same format as _unpack_contents # expects children in the same format as _unpack_contents
return pack_children(self._node, children) return _pack_normalized_children(self._node, children)
def is_readonly(self): def is_readonly(self):
return self._node.is_readonly() return self._node.is_readonly()

View File

@ -1,7 +1,6 @@
import weakref import weakref
from zope.interface import implements from zope.interface import implements
from allmydata.util.assertutil import precondition from allmydata.interfaces import INodeMaker
from allmydata.interfaces import INodeMaker, MustBeDeepImmutableError
from allmydata.immutable.filenode import ImmutableFileNode, LiteralFileNode from allmydata.immutable.filenode import ImmutableFileNode, LiteralFileNode
from allmydata.immutable.upload import Data from allmydata.immutable.upload import Data
from allmydata.mutable.filenode import MutableFileNode from allmydata.mutable.filenode import MutableFileNode
@ -97,11 +96,6 @@ class NodeMaker:
return d return d
def create_new_mutable_directory(self, initial_children={}): def create_new_mutable_directory(self, initial_children={}):
# initial_children must have metadata (i.e. {} instead of None)
for (name, (node, metadata)) in initial_children.iteritems():
precondition(isinstance(metadata, dict),
"create_new_mutable_directory requires metadata to be a dict, not None", metadata)
node.raise_error()
d = self.create_mutable_file(lambda n: d = self.create_mutable_file(lambda n:
pack_children(n, initial_children)) pack_children(n, initial_children))
d.addCallback(self._create_dirnode) d.addCallback(self._create_dirnode)
@ -110,14 +104,8 @@ class NodeMaker:
def create_immutable_directory(self, children, convergence=None): def create_immutable_directory(self, children, convergence=None):
if convergence is None: if convergence is None:
convergence = self.secret_holder.get_convergence_secret() convergence = self.secret_holder.get_convergence_secret()
for (name, (node, metadata)) in children.iteritems():
precondition(isinstance(metadata, dict),
"create_immutable_directory requires metadata to be a dict, not None", metadata)
node.raise_error()
if not node.is_allowed_in_immutable_directory():
raise MustBeDeepImmutableError("%s is not immutable" % (node,), name)
n = DummyImmutableFileNode() # writekey=None n = DummyImmutableFileNode() # writekey=None
packed = pack_children(n, children) packed = pack_children(n, children, deep_immutable=True)
uploadable = Data(packed, convergence) uploadable = Data(packed, convergence)
d = self.uploader.upload(uploadable, history=self.history) d = self.uploader.upload(uploadable, history=self.history)
d.addCallback(lambda results: self.create_from_cap(None, results.uri)) d.addCallback(lambda results: self.create_from_cap(None, results.uri))

View File

@ -1,5 +1,6 @@
import time import time
import unicodedata
from zope.interface import implements from zope.interface import implements
from twisted.trial import unittest from twisted.trial import unittest
from twisted.internet import defer from twisted.internet import defer
@ -261,7 +262,7 @@ class Dirnode(GridTestMixin, unittest.TestCase,
bad_kids2 = {one_nfd: (bad_future_node2, {})} bad_kids2 = {one_nfd: (bad_future_node2, {})}
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFail(MustBeDeepImmutableError, "bad_kids2", self.shouldFail(MustBeDeepImmutableError, "bad_kids2",
"is not immutable", "is not allowed in an immutable directory",
c.create_immutable_dirnode, c.create_immutable_dirnode,
bad_kids2)) bad_kids2))
bad_kids3 = {one_nfd: (nm.create_from_cap(one_uri), None)} bad_kids3 = {one_nfd: (nm.create_from_cap(one_uri), None)}
@ -273,13 +274,13 @@ class Dirnode(GridTestMixin, unittest.TestCase,
bad_kids4 = {one_nfd: (nm.create_from_cap(mut_write_uri), {})} bad_kids4 = {one_nfd: (nm.create_from_cap(mut_write_uri), {})}
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFail(MustBeDeepImmutableError, "bad_kids4", self.shouldFail(MustBeDeepImmutableError, "bad_kids4",
"is not immutable", "is not allowed in an immutable directory",
c.create_immutable_dirnode, c.create_immutable_dirnode,
bad_kids4)) bad_kids4))
bad_kids5 = {one_nfd: (nm.create_from_cap(mut_read_uri), {})} bad_kids5 = {one_nfd: (nm.create_from_cap(mut_read_uri), {})}
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFail(MustBeDeepImmutableError, "bad_kids5", self.shouldFail(MustBeDeepImmutableError, "bad_kids5",
"is not immutable", "is not allowed in an immutable directory",
c.create_immutable_dirnode, c.create_immutable_dirnode,
bad_kids5)) bad_kids5))
d.addCallback(lambda ign: c.create_immutable_dirnode({})) d.addCallback(lambda ign: c.create_immutable_dirnode({}))
@ -336,15 +337,15 @@ class Dirnode(GridTestMixin, unittest.TestCase,
bad_kids = {one_nfd: (nm.create_from_cap(mut_write_uri), {})} bad_kids = {one_nfd: (nm.create_from_cap(mut_write_uri), {})}
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFail(MustBeDeepImmutableError, "YZ", self.shouldFail(MustBeDeepImmutableError, "YZ",
"is not immutable", "is not allowed in an immutable directory",
n.create_subdirectory, n.create_subdirectory,
u"sub2", bad_kids, mutable=False)) u"sub2", bad_kids, mutable=False))
return d return d
d.addCallback(_made_parent) d.addCallback(_made_parent)
return d return d
def test_spaces_are_stripped_on_the_way_out(self): def test_directory_representation(self):
self.basedir = "dirnode/Dirnode/test_spaces_are_stripped_on_the_way_out" self.basedir = "dirnode/Dirnode/test_directory_representation"
self.set_up_grid() self.set_up_grid()
c = self.g.clients[0] c = self.g.clients[0]
nm = c.nodemaker nm = c.nodemaker
@ -352,6 +353,8 @@ class Dirnode(GridTestMixin, unittest.TestCase,
# This test checks that any trailing spaces in URIs are retained in the # This test checks that any trailing spaces in URIs are retained in the
# encoded directory, but stripped when we get them out of the directory. # encoded directory, but stripped when we get them out of the directory.
# See ticket #925 for why we want that. # See ticket #925 for why we want that.
# It also tests that we store child names as UTF-8 NFC, and normalize
# them again when retrieving them.
stripped_write_uri = "lafs://from_the_future\t" stripped_write_uri = "lafs://from_the_future\t"
stripped_read_uri = "lafs://readonly_from_the_future\t" stripped_read_uri = "lafs://readonly_from_the_future\t"
@ -362,9 +365,13 @@ class Dirnode(GridTestMixin, unittest.TestCase,
self.failUnlessEqual(child.get_write_uri(), spacedout_write_uri) self.failUnlessEqual(child.get_write_uri(), spacedout_write_uri)
self.failUnlessEqual(child.get_readonly_uri(), "ro." + spacedout_read_uri) self.failUnlessEqual(child.get_readonly_uri(), "ro." + spacedout_read_uri)
kids = {u"child": (child, {})} child_dottedi = u"ch\u0131\u0307ld"
d = c.create_dirnode(kids)
kids_in = {child_dottedi: (child, {}), one_nfd: (child, {})}
kids_out = {child_dottedi: (child, {}), one_nfc: (child, {})}
kids_norm = {u"child": (child, {}), one_nfc: (child, {})}
d = c.create_dirnode(kids_in)
def _created(dn): def _created(dn):
self.failUnless(isinstance(dn, dirnode.DirectoryNode)) self.failUnless(isinstance(dn, dirnode.DirectoryNode))
self.failUnless(dn.is_mutable()) self.failUnless(dn.is_mutable())
@ -377,7 +384,8 @@ class Dirnode(GridTestMixin, unittest.TestCase,
def _check_data(data): def _check_data(data):
# Decode the netstring representation of the directory to check that the # Decode the netstring representation of the directory to check that the
# spaces are retained when the URIs are stored. # spaces are retained when the URIs are stored, and that the names are stored
# as NFC.
position = 0 position = 0
numkids = 0 numkids = 0
while position < len(data): while position < len(data):
@ -386,21 +394,42 @@ class Dirnode(GridTestMixin, unittest.TestCase,
(name_utf8, ro_uri, rwcapdata, metadata_s), subpos = split_netstring(entry, 4) (name_utf8, ro_uri, rwcapdata, metadata_s), subpos = split_netstring(entry, 4)
name = name_utf8.decode("utf-8") name = name_utf8.decode("utf-8")
rw_uri = self.rootnode._decrypt_rwcapdata(rwcapdata) rw_uri = self.rootnode._decrypt_rwcapdata(rwcapdata)
self.failUnless(name in kids) self.failUnlessIn(name, kids_out)
(expected_child, ign) = kids[name] (expected_child, ign) = kids_out[name]
self.failUnlessEqual(rw_uri, expected_child.get_write_uri()) self.failUnlessEqual(rw_uri, expected_child.get_write_uri())
self.failUnlessEqual("ro." + ro_uri, expected_child.get_readonly_uri()) self.failUnlessEqual("ro." + ro_uri, expected_child.get_readonly_uri())
numkids += 1 numkids += 1
self.failUnlessEqual(numkids, 1) self.failUnlessEqual(numkids, len(kids_out))
return self.rootnode.list() return self.rootnode
d.addCallback(_check_data) d.addCallback(_check_data)
# Now when we use the real directory listing code, the trailing spaces # Mock up a hypothetical future version of Unicode that adds a canonical equivalence
# should have been stripped (and "ro." should have been prepended to the # between dotless-i + dot-above, and 'i'. That would actually be prohibited by the
# ro_uri, since it's unknown). # stability rules, but similar additions involving currently-unassigned characters
# would not be.
old_normalize = unicodedata.normalize
def future_normalize(form, s):
assert form == 'NFC', form
return old_normalize(form, s).replace(u"\u0131\u0307", u"i")
def _list(node):
unicodedata.normalize = future_normalize
d2 = node.list()
def _undo_mock(res):
unicodedata.normalize = old_normalize
return res
d2.addBoth(_undo_mock)
return d2
d.addCallback(_list)
def _check_kids(children): def _check_kids(children):
self.failUnlessEqual(set(children.keys()), set([u"child"])) # Now when we use the real directory listing code, the trailing spaces
# should have been stripped (and "ro." should have been prepended to the
# ro_uri, since it's unknown). Also the dotless-i + dot-above should have been
# normalized to 'i'.
self.failUnlessEqual(set(children.keys()), set(kids_norm.keys()))
child_node, child_metadata = children[u"child"] child_node, child_metadata = children[u"child"]
self.failUnlessEqual(child_node.get_write_uri(), stripped_write_uri) self.failUnlessEqual(child_node.get_write_uri(), stripped_write_uri)
@ -408,7 +437,7 @@ class Dirnode(GridTestMixin, unittest.TestCase,
d.addCallback(_check_kids) d.addCallback(_check_kids)
d.addCallback(lambda ign: nm.create_from_cap(self.cap.to_string())) d.addCallback(lambda ign: nm.create_from_cap(self.cap.to_string()))
d.addCallback(lambda n: n.list()) d.addCallback(_list)
d.addCallback(_check_kids) # again with dirnode recreated from cap d.addCallback(_check_kids) # again with dirnode recreated from cap
return d return d