Commit 9ff3da0c by willmcgugan@gmail.com

Implemented generic validatepath method and optimized normpath

parent eac17257
...@@ -252,6 +252,7 @@ class FS(object): ...@@ -252,6 +252,7 @@ class FS(object):
* *free_space* The free space (in bytes) available on the file system * *free_space* The free space (in bytes) available on the file system
* *total_space* The total space (in bytes) available on the file system * *total_space* The total space (in bytes) available on the file system
* *virtual* True if the filesystem defers to other filesystems * *virtual* True if the filesystem defers to other filesystems
* *invalid_path_chars* A string containing characters that may not be used in paths
FS implementations may expose non-generic meta data through a self-named namespace. e.g. ``"somefs.some_meta"`` FS implementations may expose non-generic meta data through a self-named namespace. e.g. ``"somefs.some_meta"``
...@@ -282,6 +283,38 @@ class FS(object): ...@@ -282,6 +283,38 @@ class FS(object):
return False return False
return True return True
def validatepath(self, path):
"""Validate an fs path, throws an :class:`~fs.errors.InvalidPathError` exception if validation fails.
A path is invalid if it fails to map to a path on the underlaying filesystem. The default
implementation checks for the presence of any of the characters in the meta value 'invalid_path_chars',
but implementations may have other requirements for paths.
:param path: an fs path to validatepath
:raises `fs.errors.InvalidPathError`: if `path` does not map on to a valid path on this filesystem
"""
invalid_chars = self.getmeta('invalid_path_chars', default=None)
if invalid_chars:
re_invalid_chars = getattr(self, '_re_invalid_chars', None)
if re_invalid_chars is None:
self._re_invalid_chars = re_invalid_chars = re.compile('|'.join(re.escape(c) for c in invalid_chars), re.UNICODE)
if re_invalid_chars.search(path):
raise InvalidCharsInPathError(path)
def isvalidpath(self, path):
"""Check if a path is valid on this filesystem
:param path: an fs path
"""
try:
self.validatepath(path)
except InvalidPathError:
return False
else:
return True
def getsyspath(self, path, allow_none=False): def getsyspath(self, path, allow_none=False):
"""Returns the system path (a path recognized by the OS) if one is present. """Returns the system path (a path recognized by the OS) if one is present.
......
...@@ -11,6 +11,7 @@ catch-all exception. ...@@ -11,6 +11,7 @@ catch-all exception.
__all__ = ['FSError', __all__ = ['FSError',
'CreateFailedError', 'CreateFailedError',
'PathError', 'PathError',
'InvalidPathError',
'InvalidCharsInPathError', 'InvalidCharsInPathError',
'OperationFailedError', 'OperationFailedError',
'UnsupportedError', 'UnsupportedError',
...@@ -83,7 +84,13 @@ class PathError(FSError): ...@@ -83,7 +84,13 @@ class PathError(FSError):
super(PathError,self).__init__(**kwds) super(PathError,self).__init__(**kwds)
class InvalidCharsInPathError(PathError): class InvalidPathError(PathError):
"""Base exception for fs paths that can't be mapped on to the underlaying filesystem."""
default_message = "Path is invalid on this filesystem %(path)s"
class InvalidCharsInPathError(InvalidPathError):
"""The path contains characters that are invalid on this filesystem"""
default_message = "Path contains invalid characters: %(path)s" default_message = "Path contains invalid characters: %(path)s"
......
...@@ -88,10 +88,9 @@ class OSFS(OSFSXAttrMixin, OSFSWatchMixin, FS): ...@@ -88,10 +88,9 @@ class OSFS(OSFSXAttrMixin, OSFSWatchMixin, FS):
} }
if platform.system() == 'Windows': if platform.system() == 'Windows':
_invalid_path_chars = ''.join(chr(n) for n in xrange(31)) + '\\:*?"<>|' _meta["invalid_path_chars"] = ''.join(chr(n) for n in xrange(31)) + '\\:*?"<>|'
else: else:
_invalid_path_chars = '\0' _meta["invalid_path_chars"] = '\0'
_re_invalid_path_chars = re.compile('|'.join(re.escape(c) for c in _invalid_path_chars), re.UNICODE)
def __init__(self, root_path, thread_synchronize=_thread_synchronize_default, encoding=None, create=False, dir_mode=0700, use_long_paths=True): def __init__(self, root_path, thread_synchronize=_thread_synchronize_default, encoding=None, create=False, dir_mode=0700, use_long_paths=True):
""" """
...@@ -153,13 +152,8 @@ class OSFS(OSFSXAttrMixin, OSFSWatchMixin, FS): ...@@ -153,13 +152,8 @@ class OSFS(OSFSXAttrMixin, OSFSWatchMixin, FS):
return p return p
return p.decode(self.encoding, 'replace') return p.decode(self.encoding, 'replace')
def _validate_path(self, path):
"""Raise an error if there are any invalid characters in the path"""
if self._re_invalid_path_chars.search(path):
raise InvalidCharsInPathError(path)
def getsyspath(self, path, allow_none=False): def getsyspath(self, path, allow_none=False):
self._validate_path(path) self.validatepath(path)
path = relpath(normpath(path)).replace(u"/", os.sep) path = relpath(normpath(path)).replace(u"/", os.sep)
path = os.path.join(self.root_path, path) path = os.path.join(self.root_path, path)
if not path.startswith(self.root_path): if not path.startswith(self.root_path):
......
...@@ -14,7 +14,9 @@ import re ...@@ -14,7 +14,9 @@ import re
import os import os
_requires_normalization = re.compile(r'/\.\.|\./|\.|//').search #_requires_normalization = re.compile(r'/\.\.|\./|\.|//').search
# New improved re that avoids normalizing paths that don't need it - WM
_requires_normalization = re.compile(r'/\.\.|\./|^\.$|\.$|//').search
def normpath(path): def normpath(path):
...@@ -75,11 +77,6 @@ else: ...@@ -75,11 +77,6 @@ else:
return path return path
def normospath(path):
"""Normalizes a path with os separators"""
return normpath(ospath(path))
def iteratepath(path, numsplits=None): def iteratepath(path, numsplits=None):
"""Iterate over the individual components of a path. """Iterate over the individual components of a path.
...@@ -374,7 +371,7 @@ def isprefix(path1, path2): ...@@ -374,7 +371,7 @@ def isprefix(path1, path2):
bits1.pop() bits1.pop()
if len(bits1) > len(bits2): if len(bits1) > len(bits2):
return False return False
for (bit1,bit2) in zip(bits1,bits2): for (bit1, bit2) in zip(bits1, bits2):
if bit1 != bit2: if bit1 != bit2:
return False return False
return True return True
...@@ -434,7 +431,7 @@ class PathMap(object): ...@@ -434,7 +431,7 @@ class PathMap(object):
def __init__(self): def __init__(self):
self._map = {} self._map = {}
def __getitem__(self,path): def __getitem__(self, path):
"""Get the value stored under the given path.""" """Get the value stored under the given path."""
m = self._map m = self._map
for name in iteratepath(path): for name in iteratepath(path):
...@@ -447,7 +444,7 @@ class PathMap(object): ...@@ -447,7 +444,7 @@ class PathMap(object):
except KeyError: except KeyError:
raise KeyError(path) raise KeyError(path)
def __contains__(self,path): def __contains__(self, path):
"""Check whether the given path has a value stored in the map.""" """Check whether the given path has a value stored in the map."""
try: try:
self[path] self[path]
...@@ -456,22 +453,22 @@ class PathMap(object): ...@@ -456,22 +453,22 @@ class PathMap(object):
else: else:
return True return True
def __setitem__(self,path,value): def __setitem__(self, path, value):
"""Set the value stored under the given path.""" """Set the value stored under the given path."""
m = self._map m = self._map
for name in iteratepath(path): for name in iteratepath(path):
try: try:
m = m[name] m = m[name]
except KeyError: except KeyError:
m = m.setdefault(name,{}) m = m.setdefault(name, {})
m[""] = value m[""] = value
def __delitem__(self,path): def __delitem__(self, path):
"""Delete the value stored under the given path.""" """Delete the value stored under the given path."""
ms = [[self._map,None]] ms = [[self._map, None]]
for name in iteratepath(path): for name in iteratepath(path):
try: try:
ms.append([ms[-1][0][name],None]) ms.append([ms[-1][0][name], None])
except KeyError: except KeyError:
raise KeyError(path) raise KeyError(path)
else: else:
...@@ -485,19 +482,19 @@ class PathMap(object): ...@@ -485,19 +482,19 @@ class PathMap(object):
del ms[-1] del ms[-1]
del ms[-1][0][ms[-1][1]] del ms[-1][0][ms[-1][1]]
def get(self,path,default=None): def get(self, path, default=None):
"""Get the value stored under the given path, or the given default.""" """Get the value stored under the given path, or the given default."""
try: try:
return self[path] return self[path]
except KeyError: except KeyError:
return default return default
def pop(self,path,default=None): def pop(self, path, default=None):
"""Pop the value stored under the given path, or the given default.""" """Pop the value stored under the given path, or the given default."""
ms = [[self._map,None]] ms = [[self._map, None]]
for name in iteratepath(path): for name in iteratepath(path):
try: try:
ms.append([ms[-1][0][name],None]) ms.append([ms[-1][0][name], None])
except KeyError: except KeyError:
return default return default
else: else:
...@@ -512,16 +509,16 @@ class PathMap(object): ...@@ -512,16 +509,16 @@ class PathMap(object):
del ms[-1][0][ms[-1][1]] del ms[-1][0][ms[-1][1]]
return val return val
def setdefault(self,path,value): def setdefault(self, path, value):
m = self._map m = self._map
for name in iteratepath(path): for name in iteratepath(path):
try: try:
m = m[name] m = m[name]
except KeyError: except KeyError:
m = m.setdefault(name,{}) m = m.setdefault(name, {})
return m.setdefault("",value) return m.setdefault("", value)
def clear(self,root="/"): def clear(self, root="/"):
"""Clear all entries beginning with the given root path.""" """Clear all entries beginning with the given root path."""
m = self._map m = self._map
for name in iteratepath(root): for name in iteratepath(root):
...@@ -531,7 +528,7 @@ class PathMap(object): ...@@ -531,7 +528,7 @@ class PathMap(object):
return return
m.clear() m.clear()
def iterkeys(self,root="/",m=None): def iterkeys(self, root="/", m=None):
"""Iterate over all keys beginning with the given root path.""" """Iterate over all keys beginning with the given root path."""
if m is None: if m is None:
m = self._map m = self._map
...@@ -540,12 +537,12 @@ class PathMap(object): ...@@ -540,12 +537,12 @@ class PathMap(object):
m = m[name] m = m[name]
except KeyError: except KeyError:
return return
for (nm,subm) in m.iteritems(): for (nm, subm) in m.iteritems():
if not nm: if not nm:
yield abspath(root) yield abspath(root)
else: else:
k = pathcombine(root,nm) k = pathcombine(root, nm)
for subk in self.iterkeys(k,subm): for subk in self.iterkeys(k, subm):
yield subk yield subk
def __iter__(self): def __iter__(self):
...@@ -554,7 +551,7 @@ class PathMap(object): ...@@ -554,7 +551,7 @@ class PathMap(object):
def keys(self,root="/"): def keys(self,root="/"):
return list(self.iterkeys(root)) return list(self.iterkeys(root))
def itervalues(self,root="/",m=None): def itervalues(self, root="/", m=None):
"""Iterate over all values whose keys begin with the given root path.""" """Iterate over all values whose keys begin with the given root path."""
root = normpath(root) root = normpath(root)
if m is None: if m is None:
...@@ -564,18 +561,18 @@ class PathMap(object): ...@@ -564,18 +561,18 @@ class PathMap(object):
m = m[name] m = m[name]
except KeyError: except KeyError:
return return
for (nm,subm) in m.iteritems(): for (nm, subm) in m.iteritems():
if not nm: if not nm:
yield subm yield subm
else: else:
k = pathcombine(root,nm) k = pathcombine(root, nm)
for subv in self.itervalues(k,subm): for subv in self.itervalues(k, subm):
yield subv yield subv
def values(self,root="/"): def values(self, root="/"):
return list(self.itervalues(root)) return list(self.itervalues(root))
def iteritems(self,root="/",m=None): def iteritems(self, root="/", m=None):
"""Iterate over all (key,value) pairs beginning with the given root.""" """Iterate over all (key,value) pairs beginning with the given root."""
root = normpath(root) root = normpath(root)
if m is None: if m is None:
...@@ -585,18 +582,18 @@ class PathMap(object): ...@@ -585,18 +582,18 @@ class PathMap(object):
m = m[name] m = m[name]
except KeyError: except KeyError:
return return
for (nm,subm) in m.iteritems(): for (nm, subm) in m.iteritems():
if not nm: if not nm:
yield (abspath(normpath(root)),subm) yield (abspath(normpath(root)), subm)
else: else:
k = pathcombine(root,nm) k = pathcombine(root, nm)
for (subk,subv) in self.iteritems(k,subm): for (subk, subv) in self.iteritems(k, subm):
yield (subk,subv) yield (subk, subv)
def items(self,root="/"): def items(self, root="/"):
return list(self.iteritems(root)) return list(self.iteritems(root))
def iternames(self,root="/"): def iternames(self, root="/"):
"""Iterate over all names beneath the given root path. """Iterate over all names beneath the given root path.
This is basically the equivalent of listdir() for a PathMap - it yields This is basically the equivalent of listdir() for a PathMap - it yields
...@@ -608,15 +605,17 @@ class PathMap(object): ...@@ -608,15 +605,17 @@ class PathMap(object):
m = m[name] m = m[name]
except KeyError: except KeyError:
return return
for (nm,subm) in m.iteritems(): for (nm, subm) in m.iteritems():
if nm and subm: if nm and subm:
yield nm yield nm
def names(self,root="/"): def names(self, root="/"):
return list(self.iternames(root)) return list(self.iternames(root))
_wild_chars = frozenset('*?[]!{}') _wild_chars = frozenset('*?[]!{}')
def iswildcard(path): def iswildcard(path):
"""Check if a path ends with a wildcard """Check if a path ends with a wildcard
...@@ -627,8 +626,7 @@ def iswildcard(path): ...@@ -627,8 +626,7 @@ def iswildcard(path):
""" """
assert path is not None assert path is not None
base_chars = frozenset(basename(path)) return not _wild_chars.isdisjoint(path)
return bool(base_chars.intersection(_wild_chars))
if __name__ == "__main__": if __name__ == "__main__":
print recursepath('a/b/c') print recursepath('a/b/c')
...@@ -10,6 +10,7 @@ import os ...@@ -10,6 +10,7 @@ import os
import os.path import os.path
import time import time
import tempfile import tempfile
import platform
from fs.osfs import OSFS from fs.osfs import OSFS
from fs.errors import * from fs.errors import *
...@@ -35,6 +36,11 @@ class TempFS(OSFS): ...@@ -35,6 +36,11 @@ class TempFS(OSFS):
'atomic.setcontents' : False 'atomic.setcontents' : False
} }
if platform.system() == 'Windows':
_meta["invalid_path_chars"] = ''.join(chr(n) for n in xrange(31)) + '\\:*?"<>|'
else:
_meta["invalid_path_chars"] = '\0'
def __init__(self, identifier=None, temp_dir=None, dir_mode=0700, thread_synchronize=_thread_synchronize_default): def __init__(self, identifier=None, temp_dir=None, dir_mode=0700, thread_synchronize=_thread_synchronize_default):
"""Creates a temporary Filesystem """Creates a temporary Filesystem
......
...@@ -52,10 +52,19 @@ class FSTestCases(object): ...@@ -52,10 +52,19 @@ class FSTestCases(object):
"""Check that a file exists within self.fs""" """Check that a file exists within self.fs"""
return self.fs.exists(p) return self.fs.exists(p)
def test_invalid_chars(self):
"""Check paths validate ok"""
# Will have to be overriden selectively for custom validepath methods
self.assertEqual(self.fs.validatepath(''), None)
self.assertEqual(self.fs.validatepath('.foo'), None)
self.assertEqual(self.fs.validatepath('foo'), None)
self.assertEqual(self.fs.validatepath('foo/bar'), None)
self.assert_(self.fs.isvalidpath('foo/bar'))
def test_meta(self): def test_meta(self):
"""Checks getmeta / hasmeta are functioning""" """Checks getmeta / hasmeta are functioning"""
# getmeta / hasmeta are hard to test, since there is no way to validate # getmeta / hasmeta are hard to test, since there is no way to validate
# the implementations response # the implementation's response
meta_names = ["read_only", meta_names = ["read_only",
"network", "network",
"unicode_paths"] "unicode_paths"]
......
...@@ -31,7 +31,16 @@ class TestOSFS(unittest.TestCase,FSTestCases,ThreadingTestCases): ...@@ -31,7 +31,16 @@ class TestOSFS(unittest.TestCase,FSTestCases,ThreadingTestCases):
return os.path.exists(os.path.join(self.temp_dir, relpath(p))) return os.path.exists(os.path.join(self.temp_dir, relpath(p)))
def test_invalid_chars(self): def test_invalid_chars(self):
self.assertEqual(self.fs.validatepath(''), None)
self.assertEqual(self.fs.validatepath('.foo'), None)
self.assertEqual(self.fs.validatepath('foo'), None)
self.assertEqual(self.fs.validatepath('foo/bar'), None)
self.assert_(self.fs.isvalidpath('foo/bar'))
self.assertRaises(errors.InvalidCharsInPathError, self.fs.open, 'invalid\0file', 'wb') self.assertRaises(errors.InvalidCharsInPathError, self.fs.open, 'invalid\0file', 'wb')
self.assertFalse(self.fs.isvalidpath('invalid\0file'))
self.assert_(self.fs.isvalidpath('validfile'))
self.assert_(self.fs.isvalidpath('completely_valid/path/foo.bar'))
class TestSubFS(unittest.TestCase,FSTestCases,ThreadingTestCases): class TestSubFS(unittest.TestCase,FSTestCases,ThreadingTestCases):
......
...@@ -138,6 +138,15 @@ class TestPathFunctions(unittest.TestCase): ...@@ -138,6 +138,15 @@ class TestPathFunctions(unittest.TestCase):
for path, test_basename in tests: for path, test_basename in tests:
self.assertEqual(basename(path), test_basename) self.assertEqual(basename(path), test_basename)
def test_iswildcard(self):
self.assert_(iswildcard('*'))
self.assert_(iswildcard('*.jpg'))
self.assert_(iswildcard('foo/*'))
self.assert_(iswildcard('foo/{}'))
self.assertFalse(iswildcard('foo'))
self.assertFalse(iswildcard('img.jpg'))
self.assertFalse(iswildcard('foo/bar'))
class Test_PathMap(unittest.TestCase): class Test_PathMap(unittest.TestCase):
......
...@@ -116,7 +116,7 @@ class WrapFS(FS): ...@@ -116,7 +116,7 @@ class WrapFS(FS):
transparent file compression - in this case files from the wrapped transparent file compression - in this case files from the wrapped
FS cannot be opened in append mode. FS cannot be opened in append mode.
""" """
return (mode,mode) return (mode, mode)
def __unicode__(self): def __unicode__(self):
return u"<%s: %s>" % (self.__class__.__name__,self.wrapped_fs,) return u"<%s: %s>" % (self.__class__.__name__,self.wrapped_fs,)
...@@ -134,12 +134,16 @@ class WrapFS(FS): ...@@ -134,12 +134,16 @@ class WrapFS(FS):
return self.wrapped_fs.hasmeta(meta_name) return self.wrapped_fs.hasmeta(meta_name)
@rewrite_errors @rewrite_errors
def validatepath(self, path):
return self.wrapped_fs.validatepath(self._encode(path))
@rewrite_errors
def getsyspath(self, path, allow_none=False): def getsyspath(self, path, allow_none=False):
return self.wrapped_fs.getsyspath(self._encode(path),allow_none) return self.wrapped_fs.getsyspath(self._encode(path), allow_none)
@rewrite_errors @rewrite_errors
def getpathurl(self, path, allow_none=False): def getpathurl(self, path, allow_none=False):
return self.wrapped_fs.getpathurl(self._encode(path),allow_none) return self.wrapped_fs.getpathurl(self._encode(path), allow_none)
@rewrite_errors @rewrite_errors
def hassyspath(self, path): def hassyspath(self, path):
...@@ -469,7 +473,7 @@ def wrap_fs_methods(decorator, cls=None, exclude=[]): ...@@ -469,7 +473,7 @@ def wrap_fs_methods(decorator, cls=None, exclude=[]):
wrap_fs_methods.method_names = ["open","exists","isdir","isfile","listdir", wrap_fs_methods.method_names = ["open","exists","isdir","isfile","listdir",
"makedir","remove","setcontents","removedir","rename","getinfo","copy", "makedir","remove","setcontents","removedir","rename","getinfo","copy",
"move","copydir","movedir","close","getxattr","setxattr","delxattr", "move","copydir","movedir","close","getxattr","setxattr","delxattr",
"listxattrs","getsyspath","createfile", "hasmeta", "getmeta","listdirinfo", "listxattrs","validatepath","getsyspath","createfile", "hasmeta", "getmeta","listdirinfo",
"ilistdir","ilistdirinfo"] "ilistdir","ilistdirinfo"]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment