summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMike Frysinger <vapier@google.com>2021-02-25 21:53:49 -0500
committerMike Frysinger <vapier@google.com>2021-02-28 16:07:12 +0000
commita29424ea6d6f5a38ef9c25141c9f095161dbd3ff (patch)
tree0f8772476b727db41976ca70169de854cc67dbfd
parenta00c5f40e76fd520597013ae89823711212630b3 (diff)
downloadgit-repo-a29424ea6d6f5a38ef9c25141c9f095161dbd3ff.tar.gz
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>
-rw-r--r--manifest_xml.py18
-rw-r--r--tests/test_manifest_xml.py230
2 files changed, 173 insertions, 75 deletions
diff --git a/manifest_xml.py b/manifest_xml.py
index d05f4d0a..cd5954df 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -670,6 +670,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
670 for node in manifest.childNodes: 670 for node in manifest.childNodes:
671 if node.nodeName == 'include': 671 if node.nodeName == 'include':
672 name = self._reqatt(node, 'name') 672 name = self._reqatt(node, 'name')
673 msg = self._CheckLocalPath(name)
674 if msg:
675 raise ManifestInvalidPathError(
676 '<include> invalid "name": %s: %s' % (name, msg))
673 include_groups = '' 677 include_groups = ''
674 if parent_groups: 678 if parent_groups:
675 include_groups = parent_groups 679 include_groups = parent_groups
@@ -979,6 +983,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
979 reads a <project> element from the manifest file 983 reads a <project> element from the manifest file
980 """ 984 """
981 name = self._reqatt(node, 'name') 985 name = self._reqatt(node, 'name')
986 msg = self._CheckLocalPath(name, dir_ok=True)
987 if msg:
988 raise ManifestInvalidPathError(
989 '<project> invalid "name": %s: %s' % (name, msg))
982 if parent: 990 if parent:
983 name = self._JoinName(parent.name, name) 991 name = self._JoinName(parent.name, name)
984 992
@@ -999,9 +1007,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
999 path = node.getAttribute('path') 1007 path = node.getAttribute('path')
1000 if not path: 1008 if not path:
1001 path = name 1009 path = name
1002 if path.startswith('/'): 1010 else:
1003 raise ManifestParseError("project %s path cannot be absolute in %s" % 1011 msg = self._CheckLocalPath(path, dir_ok=True)
1004 (name, self.manifestFile)) 1012 if msg:
1013 raise ManifestInvalidPathError(
1014 '<project> invalid "path": %s: %s' % (path, msg))
1005 1015
1006 rebase = XmlBool(node, 'rebase', True) 1016 rebase = XmlBool(node, 'rebase', True)
1007 sync_c = XmlBool(node, 'sync-c', False) 1017 sync_c = XmlBool(node, 'sync-c', False)
@@ -1124,7 +1134,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
1124 def _CheckLocalPath(path, dir_ok=False, cwd_dot_ok=False): 1134 def _CheckLocalPath(path, dir_ok=False, cwd_dot_ok=False):
1125 """Verify |path| is reasonable for use in filesystem paths. 1135 """Verify |path| is reasonable for use in filesystem paths.
1126 1136
1127 Used with <copyfile> & <linkfile> elements. 1137 Used with <copyfile> & <linkfile> & <project> elements.
1128 1138
1129 This only validates the |path| in isolation: it does not check against the 1139 This only validates the |path| in isolation: it does not check against the
1130 current filesystem state. Thus it is suitable as a first-past in a parser. 1140 current filesystem state. Thus it is suitable as a first-past in a parser.
diff --git a/tests/test_manifest_xml.py b/tests/test_manifest_xml.py
index 4b545140..f69e9cf8 100644
--- a/tests/test_manifest_xml.py
+++ b/tests/test_manifest_xml.py
@@ -24,6 +24,40 @@ import error
24import manifest_xml 24import manifest_xml
25 25
26 26
27# Invalid paths that we don't want in the filesystem.
28INVALID_FS_PATHS = (
29 '',
30 '.',
31 '..',
32 '../',
33 './',
34 'foo/',
35 './foo',
36 '../foo',
37 'foo/./bar',
38 'foo/../../bar',
39 '/foo',
40 './../foo',
41 '.git/foo',
42 # Check case folding.
43 '.GIT/foo',
44 'blah/.git/foo',
45 '.repo/foo',
46 '.repoconfig',
47 # Block ~ due to 8.3 filenames on Windows filesystems.
48 '~',
49 'foo~',
50 'blah/foo~',
51 # Block Unicode characters that get normalized out by filesystems.
52 u'foo\u200Cbar',
53)
54
55# Make sure platforms that use path separators (e.g. Windows) are also
56# rejected properly.
57if os.path.sep != '/':
58 INVALID_FS_PATHS += tuple(x.replace('/', os.path.sep) for x in INVALID_FS_PATHS)
59
60
27class ManifestParseTestCase(unittest.TestCase): 61class ManifestParseTestCase(unittest.TestCase):
28 """TestCase for parsing manifests.""" 62 """TestCase for parsing manifests."""
29 63
@@ -86,38 +120,7 @@ class ManifestValidateFilePaths(unittest.TestCase):
86 120
87 def test_bad_paths(self): 121 def test_bad_paths(self):
88 """Make sure bad paths (src & dest) are rejected.""" 122 """Make sure bad paths (src & dest) are rejected."""
89 PATHS = ( 123 for path in INVALID_FS_PATHS:
90 '',
91 '.',
92 '..',
93 '../',
94 './',
95 'foo/',
96 './foo',
97 '../foo',
98 'foo/./bar',
99 'foo/../../bar',
100 '/foo',
101 './../foo',
102 '.git/foo',
103 # Check case folding.
104 '.GIT/foo',
105 'blah/.git/foo',
106 '.repo/foo',
107 '.repoconfig',
108 # Block ~ due to 8.3 filenames on Windows filesystems.
109 '~',
110 'foo~',
111 'blah/foo~',
112 # Block Unicode characters that get normalized out by filesystems.
113 u'foo\u200Cbar',
114 )
115 # Make sure platforms that use path separators (e.g. Windows) are also
116 # rejected properly.
117 if os.path.sep != '/':
118 PATHS += tuple(x.replace('/', os.path.sep) for x in PATHS)
119
120 for path in PATHS:
121 self.assertRaises( 124 self.assertRaises(
122 error.ManifestInvalidPathError, self.check_both, path, 'a') 125 error.ManifestInvalidPathError, self.check_both, path, 'a')
123 self.assertRaises( 126 self.assertRaises(
@@ -248,7 +251,82 @@ class XmlManifestTests(ManifestParseTestCase):
248 '<superproject name="superproject"/>' + 251 '<superproject name="superproject"/>' +
249 '</manifest>') 252 '</manifest>')
250 253
251 def test_project_group(self): 254
255class IncludeElementTests(ManifestParseTestCase):
256 """Tests for <include>."""
257
258 def test_group_levels(self):
259 root_m = os.path.join(self.manifest_dir, 'root.xml')
260 with open(root_m, 'w') as fp:
261 fp.write("""
262<manifest>
263 <remote name="test-remote" fetch="http://localhost" />
264 <default remote="test-remote" revision="refs/heads/main" />
265 <include name="level1.xml" groups="level1-group" />
266 <project name="root-name1" path="root-path1" />
267 <project name="root-name2" path="root-path2" groups="r2g1,r2g2" />
268</manifest>
269""")
270 with open(os.path.join(self.manifest_dir, 'level1.xml'), 'w') as fp:
271 fp.write("""
272<manifest>
273 <include name="level2.xml" groups="level2-group" />
274 <project name="level1-name1" path="level1-path1" />
275</manifest>
276""")
277 with open(os.path.join(self.manifest_dir, 'level2.xml'), 'w') as fp:
278 fp.write("""
279<manifest>
280 <project name="level2-name1" path="level2-path1" groups="l2g1,l2g2" />
281</manifest>
282""")
283 include_m = manifest_xml.XmlManifest(self.repodir, root_m)
284 for proj in include_m.projects:
285 if proj.name == 'root-name1':
286 # Check include group not set on root level proj.
287 self.assertNotIn('level1-group', proj.groups)
288 if proj.name == 'root-name2':
289 # Check root proj group not removed.
290 self.assertIn('r2g1', proj.groups)
291 if proj.name == 'level1-name1':
292 # Check level1 proj has inherited group level 1.
293 self.assertIn('level1-group', proj.groups)
294 if proj.name == 'level2-name1':
295 # Check level2 proj has inherited group levels 1 and 2.
296 self.assertIn('level1-group', proj.groups)
297 self.assertIn('level2-group', proj.groups)
298 # Check level2 proj group not removed.
299 self.assertIn('l2g1', proj.groups)
300
301 def test_bad_name_checks(self):
302 """Check handling of bad name attribute."""
303 def parse(name):
304 manifest = self.getXmlManifest(f"""
305<manifest>
306 <remote name="default-remote" fetch="http://localhost" />
307 <default remote="default-remote" revision="refs/heads/main" />
308 <include name="{name}" />
309</manifest>
310""")
311 # Force the manifest to be parsed.
312 manifest.ToXml()
313
314 # Handle empty name explicitly because a different codepath rejects it.
315 with self.assertRaises(error.ManifestParseError):
316 parse('')
317
318 for path in INVALID_FS_PATHS:
319 if not path:
320 continue
321
322 with self.assertRaises(error.ManifestInvalidPathError):
323 parse(path)
324
325
326class ProjectElementTests(ManifestParseTestCase):
327 """Tests for <project>."""
328
329 def test_group(self):
252 """Check project group settings.""" 330 """Check project group settings."""
253 manifest = self.getXmlManifest(""" 331 manifest = self.getXmlManifest("""
254<manifest> 332<manifest>
@@ -272,7 +350,7 @@ class XmlManifestTests(ManifestParseTestCase):
272 result['extras'], 350 result['extras'],
273 ['g1', 'g2', 'g1', 'name:extras', 'all', 'path:path']) 351 ['g1', 'g2', 'g1', 'name:extras', 'all', 'path:path'])
274 352
275 def test_project_set_revision_id(self): 353 def test_set_revision_id(self):
276 """Check setting of project's revisionId.""" 354 """Check setting of project's revisionId."""
277 manifest = self.getXmlManifest(""" 355 manifest = self.getXmlManifest("""
278<manifest> 356<manifest>
@@ -292,51 +370,61 @@ class XmlManifestTests(ManifestParseTestCase):
292 '<project name="test-name" revision="ABCDEF"/>' + 370 '<project name="test-name" revision="ABCDEF"/>' +
293 '</manifest>') 371 '</manifest>')
294 372
295 def test_include_levels(self): 373 def test_trailing_slash(self):
296 root_m = os.path.join(self.manifest_dir, 'root.xml') 374 """Check handling of trailing slashes in attributes."""
297 with open(root_m, 'w') as fp: 375 def parse(name, path):
298 fp.write(""" 376 return self.getXmlManifest(f"""
299<manifest>
300 <remote name="test-remote" fetch="http://localhost" />
301 <default remote="test-remote" revision="refs/heads/main" />
302 <include name="level1.xml" groups="level1-group" />
303 <project name="root-name1" path="root-path1" />
304 <project name="root-name2" path="root-path2" groups="r2g1,r2g2" />
305</manifest>
306""")
307 with open(os.path.join(self.manifest_dir, 'level1.xml'), 'w') as fp:
308 fp.write("""
309<manifest> 377<manifest>
310 <include name="level2.xml" groups="level2-group" /> 378 <remote name="default-remote" fetch="http://localhost" />
311 <project name="level1-name1" path="level1-path1" /> 379 <default remote="default-remote" revision="refs/heads/main" />
380 <project name="{name}" path="{path}" />
312</manifest> 381</manifest>
313""") 382""")
314 with open(os.path.join(self.manifest_dir, 'level2.xml'), 'w') as fp: 383
315 fp.write(""" 384 manifest = parse('a/path/', 'foo')
385 self.assertEqual(manifest.projects[0].gitdir,
386 os.path.join(self.tempdir, '.repo/projects/foo.git'))
387 self.assertEqual(manifest.projects[0].objdir,
388 os.path.join(self.tempdir, '.repo/project-objects/a/path.git'))
389
390 manifest = parse('a/path', 'foo/')
391 self.assertEqual(manifest.projects[0].gitdir,
392 os.path.join(self.tempdir, '.repo/projects/foo.git'))
393 self.assertEqual(manifest.projects[0].objdir,
394 os.path.join(self.tempdir, '.repo/project-objects/a/path.git'))
395
396 def test_bad_path_name_checks(self):
397 """Check handling of bad path & name attributes."""
398 def parse(name, path):
399 manifest = self.getXmlManifest(f"""
316<manifest> 400<manifest>
317 <project name="level2-name1" path="level2-path1" groups="l2g1,l2g2" /> 401 <remote name="default-remote" fetch="http://localhost" />
402 <default remote="default-remote" revision="refs/heads/main" />
403 <project name="{name}" path="{path}" />
318</manifest> 404</manifest>
319""") 405""")
320 include_m = manifest_xml.XmlManifest(self.repodir, root_m) 406 # Force the manifest to be parsed.
321 for proj in include_m.projects: 407 manifest.ToXml()
322 if proj.name == 'root-name1': 408
323 # Check include group not set on root level proj. 409 # Verify the parser is valid by default to avoid buggy tests below.
324 self.assertNotIn('level1-group', proj.groups) 410 parse('ok', 'ok')
325 if proj.name == 'root-name2': 411
326 # Check root proj group not removed. 412 # Handle empty name explicitly because a different codepath rejects it.
327 self.assertIn('r2g1', proj.groups) 413 # Empty path is OK because it defaults to the name field.
328 if proj.name == 'level1-name1': 414 with self.assertRaises(error.ManifestParseError):
329 # Check level1 proj has inherited group level 1. 415 parse('', 'ok')
330 self.assertIn('level1-group', proj.groups) 416
331 if proj.name == 'level2-name1': 417 for path in INVALID_FS_PATHS:
332 # Check level2 proj has inherited group levels 1 and 2. 418 if not path or path.endswith('/'):
333 self.assertIn('level1-group', proj.groups) 419 continue
334 self.assertIn('level2-group', proj.groups) 420
335 # Check level2 proj group not removed. 421 with self.assertRaises(error.ManifestInvalidPathError):
336 self.assertIn('l2g1', proj.groups) 422 parse(path, 'ok')
423 with self.assertRaises(error.ManifestInvalidPathError):
424 parse('ok', path)
337 425
338 426
339class SuperProjectTests(ManifestParseTestCase): 427class SuperProjectElementTests(ManifestParseTestCase):
340 """Tests for <superproject>.""" 428 """Tests for <superproject>."""
341 429
342 def test_superproject(self): 430 def test_superproject(self):