manifest: validate project name & path and include name attributes

These attribute values are used to construct local filesystem paths,
so apply the existing filesystem checks to them.

Bug: https://crbug.com/gerrit/14156
Change-Id: Ibcceecd60fa74f0eb97cd9ed1a9792e139534ed4
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/298443
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
This commit is contained in:
Mike Frysinger 2021-02-25 21:53:49 -05:00
parent a00c5f40e7
commit a29424ea6d
2 changed files with 178 additions and 80 deletions

View File

@ -670,6 +670,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
for node in manifest.childNodes: for node in manifest.childNodes:
if node.nodeName == 'include': if node.nodeName == 'include':
name = self._reqatt(node, 'name') name = self._reqatt(node, 'name')
msg = self._CheckLocalPath(name)
if msg:
raise ManifestInvalidPathError(
'<include> invalid "name": %s: %s' % (name, msg))
include_groups = '' include_groups = ''
if parent_groups: if parent_groups:
include_groups = parent_groups include_groups = parent_groups
@ -979,6 +983,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
reads a <project> element from the manifest file reads a <project> element from the manifest file
""" """
name = self._reqatt(node, 'name') name = self._reqatt(node, 'name')
msg = self._CheckLocalPath(name, dir_ok=True)
if msg:
raise ManifestInvalidPathError(
'<project> invalid "name": %s: %s' % (name, msg))
if parent: if parent:
name = self._JoinName(parent.name, name) name = self._JoinName(parent.name, name)
@ -999,9 +1007,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
path = node.getAttribute('path') path = node.getAttribute('path')
if not path: if not path:
path = name path = name
if path.startswith('/'): else:
raise ManifestParseError("project %s path cannot be absolute in %s" % msg = self._CheckLocalPath(path, dir_ok=True)
(name, self.manifestFile)) if msg:
raise ManifestInvalidPathError(
'<project> invalid "path": %s: %s' % (path, msg))
rebase = XmlBool(node, 'rebase', True) rebase = XmlBool(node, 'rebase', True)
sync_c = XmlBool(node, 'sync-c', False) sync_c = XmlBool(node, 'sync-c', False)
@ -1124,7 +1134,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
def _CheckLocalPath(path, dir_ok=False, cwd_dot_ok=False): def _CheckLocalPath(path, dir_ok=False, cwd_dot_ok=False):
"""Verify |path| is reasonable for use in filesystem paths. """Verify |path| is reasonable for use in filesystem paths.
Used with <copyfile> & <linkfile> elements. Used with <copyfile> & <linkfile> & <project> elements.
This only validates the |path| in isolation: it does not check against the This only validates the |path| in isolation: it does not check against the
current filesystem state. Thus it is suitable as a first-past in a parser. current filesystem state. Thus it is suitable as a first-past in a parser.

View File

@ -24,6 +24,40 @@ import error
import manifest_xml import manifest_xml
# Invalid paths that we don't want in the filesystem.
INVALID_FS_PATHS = (
'',
'.',
'..',
'../',
'./',
'foo/',
'./foo',
'../foo',
'foo/./bar',
'foo/../../bar',
'/foo',
'./../foo',
'.git/foo',
# Check case folding.
'.GIT/foo',
'blah/.git/foo',
'.repo/foo',
'.repoconfig',
# Block ~ due to 8.3 filenames on Windows filesystems.
'~',
'foo~',
'blah/foo~',
# Block Unicode characters that get normalized out by filesystems.
u'foo\u200Cbar',
)
# Make sure platforms that use path separators (e.g. Windows) are also
# rejected properly.
if os.path.sep != '/':
INVALID_FS_PATHS += tuple(x.replace('/', os.path.sep) for x in INVALID_FS_PATHS)
class ManifestParseTestCase(unittest.TestCase): class ManifestParseTestCase(unittest.TestCase):
"""TestCase for parsing manifests.""" """TestCase for parsing manifests."""
@ -86,38 +120,7 @@ class ManifestValidateFilePaths(unittest.TestCase):
def test_bad_paths(self): def test_bad_paths(self):
"""Make sure bad paths (src & dest) are rejected.""" """Make sure bad paths (src & dest) are rejected."""
PATHS = ( for path in INVALID_FS_PATHS:
'',
'.',
'..',
'../',
'./',
'foo/',
'./foo',
'../foo',
'foo/./bar',
'foo/../../bar',
'/foo',
'./../foo',
'.git/foo',
# Check case folding.
'.GIT/foo',
'blah/.git/foo',
'.repo/foo',
'.repoconfig',
# Block ~ due to 8.3 filenames on Windows filesystems.
'~',
'foo~',
'blah/foo~',
# Block Unicode characters that get normalized out by filesystems.
u'foo\u200Cbar',
)
# Make sure platforms that use path separators (e.g. Windows) are also
# rejected properly.
if os.path.sep != '/':
PATHS += tuple(x.replace('/', os.path.sep) for x in PATHS)
for path in PATHS:
self.assertRaises( self.assertRaises(
error.ManifestInvalidPathError, self.check_both, path, 'a') error.ManifestInvalidPathError, self.check_both, path, 'a')
self.assertRaises( self.assertRaises(
@ -248,51 +251,11 @@ class XmlManifestTests(ManifestParseTestCase):
'<superproject name="superproject"/>' + '<superproject name="superproject"/>' +
'</manifest>') '</manifest>')
def test_project_group(self):
"""Check project group settings."""
manifest = self.getXmlManifest("""
<manifest>
<remote name="test-remote" fetch="http://localhost" />
<default remote="test-remote" revision="refs/heads/main" />
<project name="test-name" path="test-path"/>
<project name="extras" path="path" groups="g1,g2,g1"/>
</manifest>
""")
self.assertEqual(len(manifest.projects), 2)
# Ordering isn't guaranteed.
result = {
manifest.projects[0].name: manifest.projects[0].groups,
manifest.projects[1].name: manifest.projects[1].groups,
}
project = manifest.projects[0]
self.assertCountEqual(
result['test-name'],
['name:test-name', 'all', 'path:test-path'])
self.assertCountEqual(
result['extras'],
['g1', 'g2', 'g1', 'name:extras', 'all', 'path:path'])
def test_project_set_revision_id(self): class IncludeElementTests(ManifestParseTestCase):
"""Check setting of project's revisionId.""" """Tests for <include>."""
manifest = self.getXmlManifest("""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
<default remote="default-remote" revision="refs/heads/main" />
<project name="test-name"/>
</manifest>
""")
self.assertEqual(len(manifest.projects), 1)
project = manifest.projects[0]
project.SetRevisionId('ABCDEF')
self.assertEqual(
manifest.ToXml().toxml(),
'<?xml version="1.0" ?><manifest>' +
'<remote name="default-remote" fetch="http://localhost"/>' +
'<default remote="default-remote" revision="refs/heads/main"/>' +
'<project name="test-name" revision="ABCDEF"/>' +
'</manifest>')
def test_include_levels(self): def test_group_levels(self):
root_m = os.path.join(self.manifest_dir, 'root.xml') root_m = os.path.join(self.manifest_dir, 'root.xml')
with open(root_m, 'w') as fp: with open(root_m, 'w') as fp:
fp.write(""" fp.write("""
@ -335,8 +298,133 @@ class XmlManifestTests(ManifestParseTestCase):
# Check level2 proj group not removed. # Check level2 proj group not removed.
self.assertIn('l2g1', proj.groups) self.assertIn('l2g1', proj.groups)
def test_bad_name_checks(self):
"""Check handling of bad name attribute."""
def parse(name):
manifest = self.getXmlManifest(f"""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
<default remote="default-remote" revision="refs/heads/main" />
<include name="{name}" />
</manifest>
""")
# Force the manifest to be parsed.
manifest.ToXml()
class SuperProjectTests(ManifestParseTestCase): # Handle empty name explicitly because a different codepath rejects it.
with self.assertRaises(error.ManifestParseError):
parse('')
for path in INVALID_FS_PATHS:
if not path:
continue
with self.assertRaises(error.ManifestInvalidPathError):
parse(path)
class ProjectElementTests(ManifestParseTestCase):
"""Tests for <project>."""
def test_group(self):
"""Check project group settings."""
manifest = self.getXmlManifest("""
<manifest>
<remote name="test-remote" fetch="http://localhost" />
<default remote="test-remote" revision="refs/heads/main" />
<project name="test-name" path="test-path"/>
<project name="extras" path="path" groups="g1,g2,g1"/>
</manifest>
""")
self.assertEqual(len(manifest.projects), 2)
# Ordering isn't guaranteed.
result = {
manifest.projects[0].name: manifest.projects[0].groups,
manifest.projects[1].name: manifest.projects[1].groups,
}
project = manifest.projects[0]
self.assertCountEqual(
result['test-name'],
['name:test-name', 'all', 'path:test-path'])
self.assertCountEqual(
result['extras'],
['g1', 'g2', 'g1', 'name:extras', 'all', 'path:path'])
def test_set_revision_id(self):
"""Check setting of project's revisionId."""
manifest = self.getXmlManifest("""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
<default remote="default-remote" revision="refs/heads/main" />
<project name="test-name"/>
</manifest>
""")
self.assertEqual(len(manifest.projects), 1)
project = manifest.projects[0]
project.SetRevisionId('ABCDEF')
self.assertEqual(
manifest.ToXml().toxml(),
'<?xml version="1.0" ?><manifest>' +
'<remote name="default-remote" fetch="http://localhost"/>' +
'<default remote="default-remote" revision="refs/heads/main"/>' +
'<project name="test-name" revision="ABCDEF"/>' +
'</manifest>')
def test_trailing_slash(self):
"""Check handling of trailing slashes in attributes."""
def parse(name, path):
return self.getXmlManifest(f"""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
<default remote="default-remote" revision="refs/heads/main" />
<project name="{name}" path="{path}" />
</manifest>
""")
manifest = parse('a/path/', 'foo')
self.assertEqual(manifest.projects[0].gitdir,
os.path.join(self.tempdir, '.repo/projects/foo.git'))
self.assertEqual(manifest.projects[0].objdir,
os.path.join(self.tempdir, '.repo/project-objects/a/path.git'))
manifest = parse('a/path', 'foo/')
self.assertEqual(manifest.projects[0].gitdir,
os.path.join(self.tempdir, '.repo/projects/foo.git'))
self.assertEqual(manifest.projects[0].objdir,
os.path.join(self.tempdir, '.repo/project-objects/a/path.git'))
def test_bad_path_name_checks(self):
"""Check handling of bad path & name attributes."""
def parse(name, path):
manifest = self.getXmlManifest(f"""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
<default remote="default-remote" revision="refs/heads/main" />
<project name="{name}" path="{path}" />
</manifest>
""")
# Force the manifest to be parsed.
manifest.ToXml()
# Verify the parser is valid by default to avoid buggy tests below.
parse('ok', 'ok')
# Handle empty name explicitly because a different codepath rejects it.
# Empty path is OK because it defaults to the name field.
with self.assertRaises(error.ManifestParseError):
parse('', 'ok')
for path in INVALID_FS_PATHS:
if not path or path.endswith('/'):
continue
with self.assertRaises(error.ManifestInvalidPathError):
parse(path, 'ok')
with self.assertRaises(error.ManifestInvalidPathError):
parse('ok', path)
class SuperProjectElementTests(ManifestParseTestCase):
"""Tests for <superproject>.""" """Tests for <superproject>."""
def test_superproject(self): def test_superproject(self):