##// END OF EJS Templates
pathutil: use `finddirs_rev_noroot` instead of `parts`...
Arseniy Alekseyev -
r50803:789e152a default
parent child Browse files
Show More
@@ -1,385 +1,383 b''
1 import contextlib
1 import contextlib
2 import errno
2 import errno
3 import os
3 import os
4 import posixpath
4 import posixpath
5 import stat
5 import stat
6
6
7 from typing import (
7 from typing import (
8 Any,
8 Any,
9 Callable,
9 Callable,
10 Iterator,
10 Iterator,
11 Optional,
11 Optional,
12 )
12 )
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 encoding,
16 encoding,
17 error,
17 error,
18 policy,
18 policy,
19 pycompat,
19 pycompat,
20 util,
20 util,
21 )
21 )
22
22
23 rustdirs = policy.importrust('dirstate', 'Dirs')
23 rustdirs = policy.importrust('dirstate', 'Dirs')
24 parsers = policy.importmod('parsers')
24 parsers = policy.importmod('parsers')
25
25
26
26
27 def _lowerclean(s):
27 def _lowerclean(s):
28 # type: (bytes) -> bytes
28 # type: (bytes) -> bytes
29 return encoding.hfsignoreclean(s.lower())
29 return encoding.hfsignoreclean(s.lower())
30
30
31
31
32 class pathauditor:
32 class pathauditor:
33 """ensure that a filesystem path contains no banned components.
33 """ensure that a filesystem path contains no banned components.
34 the following properties of a path are checked:
34 the following properties of a path are checked:
35
35
36 - ends with a directory separator
36 - ends with a directory separator
37 - under top-level .hg
37 - under top-level .hg
38 - starts at the root of a windows drive
38 - starts at the root of a windows drive
39 - contains ".."
39 - contains ".."
40
40
41 More check are also done about the file system states:
41 More check are also done about the file system states:
42 - traverses a symlink (e.g. a/symlink_here/b)
42 - traverses a symlink (e.g. a/symlink_here/b)
43 - inside a nested repository (a callback can be used to approve
43 - inside a nested repository (a callback can be used to approve
44 some nested repositories, e.g., subrepositories)
44 some nested repositories, e.g., subrepositories)
45
45
46 The file system checks are only done when 'realfs' is set to True (the
46 The file system checks are only done when 'realfs' is set to True (the
47 default). They should be disable then we are auditing path for operation on
47 default). They should be disable then we are auditing path for operation on
48 stored history.
48 stored history.
49
49
50 If 'cached' is set to True, audited paths and sub-directories are cached.
50 If 'cached' is set to True, audited paths and sub-directories are cached.
51 Be careful to not keep the cache of unmanaged directories for long because
51 Be careful to not keep the cache of unmanaged directories for long because
52 audited paths may be replaced with symlinks.
52 audited paths may be replaced with symlinks.
53 """
53 """
54
54
55 def __init__(self, root, callback=None, realfs=True, cached=False):
55 def __init__(self, root, callback=None, realfs=True, cached=False):
56 self.audited = set()
56 self.audited = set()
57 self.auditeddir = dict()
57 self.auditeddir = dict()
58 self.root = root
58 self.root = root
59 self._realfs = realfs
59 self._realfs = realfs
60 self._cached = cached
60 self._cached = cached
61 self.callback = callback
61 self.callback = callback
62 if os.path.lexists(root) and not util.fscasesensitive(root):
62 if os.path.lexists(root) and not util.fscasesensitive(root):
63 self.normcase = util.normcase
63 self.normcase = util.normcase
64 else:
64 else:
65 self.normcase = lambda x: x
65 self.normcase = lambda x: x
66
66
67 def __call__(self, path, mode=None):
67 def __call__(self, path, mode=None):
68 # type: (bytes, Optional[Any]) -> None
68 # type: (bytes, Optional[Any]) -> None
69 """Check the relative path.
69 """Check the relative path.
70 path may contain a pattern (e.g. foodir/**.txt)"""
70 path may contain a pattern (e.g. foodir/**.txt)"""
71
71
72 path = util.localpath(path)
72 path = util.localpath(path)
73 if path in self.audited:
73 if path in self.audited:
74 return
74 return
75 # AIX ignores "/" at end of path, others raise EISDIR.
75 # AIX ignores "/" at end of path, others raise EISDIR.
76 if util.endswithsep(path):
76 if util.endswithsep(path):
77 raise error.InputError(
77 raise error.InputError(
78 _(b"path ends in directory separator: %s") % path
78 _(b"path ends in directory separator: %s") % path
79 )
79 )
80 parts = util.splitpath(path)
80 parts = util.splitpath(path)
81 if (
81 if (
82 os.path.splitdrive(path)[0]
82 os.path.splitdrive(path)[0]
83 or _lowerclean(parts[0]) in (b'.hg', b'.hg.', b'')
83 or _lowerclean(parts[0]) in (b'.hg', b'.hg.', b'')
84 or pycompat.ospardir in parts
84 or pycompat.ospardir in parts
85 ):
85 ):
86 raise error.InputError(
86 raise error.InputError(
87 _(b"path contains illegal component: %s") % path
87 _(b"path contains illegal component: %s") % path
88 )
88 )
89 # Windows shortname aliases
89 # Windows shortname aliases
90 if b"~" in path:
90 if b"~" in path:
91 for p in parts:
91 for p in parts:
92 if b"~" in p:
92 if b"~" in p:
93 first, last = p.split(b"~", 1)
93 first, last = p.split(b"~", 1)
94 if last.isdigit() and first.upper() in [b"HG", b"HG8B6C"]:
94 if last.isdigit() and first.upper() in [b"HG", b"HG8B6C"]:
95 raise error.InputError(
95 raise error.InputError(
96 _(b"path contains illegal component: %s") % path
96 _(b"path contains illegal component: %s") % path
97 )
97 )
98 if b'.hg' in _lowerclean(path):
98 if b'.hg' in _lowerclean(path):
99 lparts = [_lowerclean(p) for p in parts]
99 lparts = [_lowerclean(p) for p in parts]
100 for p in b'.hg', b'.hg.':
100 for p in b'.hg', b'.hg.':
101 if p in lparts[1:]:
101 if p in lparts[1:]:
102 pos = lparts.index(p)
102 pos = lparts.index(p)
103 base = os.path.join(*parts[:pos])
103 base = os.path.join(*parts[:pos])
104 raise error.InputError(
104 raise error.InputError(
105 _(b"path '%s' is inside nested repo %r")
105 _(b"path '%s' is inside nested repo %r")
106 % (path, pycompat.bytestr(base))
106 % (path, pycompat.bytestr(base))
107 )
107 )
108
108
109 if self._realfs:
109 if self._realfs:
110 parts.pop()
111 # It's important that we check the path parts starting from the root.
110 # It's important that we check the path parts starting from the root.
112 # We don't want to add "foo/bar/baz" to auditeddir before checking if
111 # We don't want to add "foo/bar/baz" to auditeddir before checking if
113 # there's a "foo/.hg" directory. This also means we won't accidentally
112 # there's a "foo/.hg" directory. This also means we won't accidentally
114 # traverse a symlink into some other filesystem (which is potentially
113 # traverse a symlink into some other filesystem (which is potentially
115 # expensive to access).
114 # expensive to access).
116 for i in range(len(parts)):
115 for prefix in finddirs_rev_noroot(path):
117 prefix = pycompat.ossep.join(parts[: i + 1])
118 if prefix in self.auditeddir:
116 if prefix in self.auditeddir:
119 res = self.auditeddir[prefix]
117 res = self.auditeddir[prefix]
120 else:
118 else:
121 res = self._checkfs_exists(prefix, path)
119 res = self._checkfs_exists(prefix, path)
122 if self._cached:
120 if self._cached:
123 self.auditeddir[prefix] = res
121 self.auditeddir[prefix] = res
124 if not res:
122 if not res:
125 break
123 break
126
124
127 if self._cached:
125 if self._cached:
128 self.audited.add(path)
126 self.audited.add(path)
129
127
130 def _checkfs_exists(self, prefix: bytes, path: bytes) -> bool:
128 def _checkfs_exists(self, prefix: bytes, path: bytes) -> bool:
131 """raise exception if a file system backed check fails.
129 """raise exception if a file system backed check fails.
132
130
133 Return a bool that indicates that the directory (or file) exists."""
131 Return a bool that indicates that the directory (or file) exists."""
134 curpath = os.path.join(self.root, prefix)
132 curpath = os.path.join(self.root, prefix)
135 try:
133 try:
136 st = os.lstat(curpath)
134 st = os.lstat(curpath)
137 except OSError as err:
135 except OSError as err:
138 if err.errno == errno.ENOENT:
136 if err.errno == errno.ENOENT:
139 return False
137 return False
140 # EINVAL can be raised as invalid path syntax under win32.
138 # EINVAL can be raised as invalid path syntax under win32.
141 # They must be ignored for patterns can be checked too.
139 # They must be ignored for patterns can be checked too.
142 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
140 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
143 raise
141 raise
144 else:
142 else:
145 if stat.S_ISLNK(st.st_mode):
143 if stat.S_ISLNK(st.st_mode):
146 msg = _(b'path %r traverses symbolic link %r') % (
144 msg = _(b'path %r traverses symbolic link %r') % (
147 pycompat.bytestr(path),
145 pycompat.bytestr(path),
148 pycompat.bytestr(prefix),
146 pycompat.bytestr(prefix),
149 )
147 )
150 raise error.Abort(msg)
148 raise error.Abort(msg)
151 elif stat.S_ISDIR(st.st_mode) and os.path.isdir(
149 elif stat.S_ISDIR(st.st_mode) and os.path.isdir(
152 os.path.join(curpath, b'.hg')
150 os.path.join(curpath, b'.hg')
153 ):
151 ):
154 if not self.callback or not self.callback(curpath):
152 if not self.callback or not self.callback(curpath):
155 msg = _(b"path '%s' is inside nested repo %r")
153 msg = _(b"path '%s' is inside nested repo %r")
156 raise error.Abort(msg % (path, pycompat.bytestr(prefix)))
154 raise error.Abort(msg % (path, pycompat.bytestr(prefix)))
157 return True
155 return True
158
156
159 def check(self, path):
157 def check(self, path):
160 # type: (bytes) -> bool
158 # type: (bytes) -> bool
161 try:
159 try:
162 self(path)
160 self(path)
163 return True
161 return True
164 except (OSError, error.Abort):
162 except (OSError, error.Abort):
165 return False
163 return False
166
164
167 @contextlib.contextmanager
165 @contextlib.contextmanager
168 def cached(self):
166 def cached(self):
169 if self._cached:
167 if self._cached:
170 yield
168 yield
171 else:
169 else:
172 try:
170 try:
173 self._cached = True
171 self._cached = True
174 yield
172 yield
175 finally:
173 finally:
176 self.audited.clear()
174 self.audited.clear()
177 self.auditeddir.clear()
175 self.auditeddir.clear()
178 self._cached = False
176 self._cached = False
179
177
180
178
181 def canonpath(root, cwd, myname, auditor=None):
179 def canonpath(root, cwd, myname, auditor=None):
182 # type: (bytes, bytes, bytes, Optional[pathauditor]) -> bytes
180 # type: (bytes, bytes, bytes, Optional[pathauditor]) -> bytes
183 """return the canonical path of myname, given cwd and root
181 """return the canonical path of myname, given cwd and root
184
182
185 >>> def check(root, cwd, myname):
183 >>> def check(root, cwd, myname):
186 ... a = pathauditor(root, realfs=False)
184 ... a = pathauditor(root, realfs=False)
187 ... try:
185 ... try:
188 ... return canonpath(root, cwd, myname, a)
186 ... return canonpath(root, cwd, myname, a)
189 ... except error.Abort:
187 ... except error.Abort:
190 ... return 'aborted'
188 ... return 'aborted'
191 >>> def unixonly(root, cwd, myname, expected='aborted'):
189 >>> def unixonly(root, cwd, myname, expected='aborted'):
192 ... if pycompat.iswindows:
190 ... if pycompat.iswindows:
193 ... return expected
191 ... return expected
194 ... return check(root, cwd, myname)
192 ... return check(root, cwd, myname)
195 >>> def winonly(root, cwd, myname, expected='aborted'):
193 >>> def winonly(root, cwd, myname, expected='aborted'):
196 ... if not pycompat.iswindows:
194 ... if not pycompat.iswindows:
197 ... return expected
195 ... return expected
198 ... return check(root, cwd, myname)
196 ... return check(root, cwd, myname)
199 >>> winonly(b'd:\\\\repo', b'c:\\\\dir', b'filename')
197 >>> winonly(b'd:\\\\repo', b'c:\\\\dir', b'filename')
200 'aborted'
198 'aborted'
201 >>> winonly(b'c:\\\\repo', b'c:\\\\dir', b'filename')
199 >>> winonly(b'c:\\\\repo', b'c:\\\\dir', b'filename')
202 'aborted'
200 'aborted'
203 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'filename')
201 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'filename')
204 'aborted'
202 'aborted'
205 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'repo\\\\filename',
203 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'repo\\\\filename',
206 ... b'filename')
204 ... b'filename')
207 'filename'
205 'filename'
208 >>> winonly(b'c:\\\\repo', b'c:\\\\repo', b'filename', b'filename')
206 >>> winonly(b'c:\\\\repo', b'c:\\\\repo', b'filename', b'filename')
209 'filename'
207 'filename'
210 >>> winonly(b'c:\\\\repo', b'c:\\\\repo\\\\subdir', b'filename',
208 >>> winonly(b'c:\\\\repo', b'c:\\\\repo\\\\subdir', b'filename',
211 ... b'subdir/filename')
209 ... b'subdir/filename')
212 'subdir/filename'
210 'subdir/filename'
213 >>> unixonly(b'/repo', b'/dir', b'filename')
211 >>> unixonly(b'/repo', b'/dir', b'filename')
214 'aborted'
212 'aborted'
215 >>> unixonly(b'/repo', b'/', b'filename')
213 >>> unixonly(b'/repo', b'/', b'filename')
216 'aborted'
214 'aborted'
217 >>> unixonly(b'/repo', b'/', b'repo/filename', b'filename')
215 >>> unixonly(b'/repo', b'/', b'repo/filename', b'filename')
218 'filename'
216 'filename'
219 >>> unixonly(b'/repo', b'/repo', b'filename', b'filename')
217 >>> unixonly(b'/repo', b'/repo', b'filename', b'filename')
220 'filename'
218 'filename'
221 >>> unixonly(b'/repo', b'/repo/subdir', b'filename', b'subdir/filename')
219 >>> unixonly(b'/repo', b'/repo/subdir', b'filename', b'subdir/filename')
222 'subdir/filename'
220 'subdir/filename'
223 """
221 """
224 if util.endswithsep(root):
222 if util.endswithsep(root):
225 rootsep = root
223 rootsep = root
226 else:
224 else:
227 rootsep = root + pycompat.ossep
225 rootsep = root + pycompat.ossep
228 name = myname
226 name = myname
229 if not os.path.isabs(name):
227 if not os.path.isabs(name):
230 name = os.path.join(root, cwd, name)
228 name = os.path.join(root, cwd, name)
231 name = os.path.normpath(name)
229 name = os.path.normpath(name)
232 if auditor is None:
230 if auditor is None:
233 auditor = pathauditor(root)
231 auditor = pathauditor(root)
234 if name != rootsep and name.startswith(rootsep):
232 if name != rootsep and name.startswith(rootsep):
235 name = name[len(rootsep) :]
233 name = name[len(rootsep) :]
236 auditor(name)
234 auditor(name)
237 return util.pconvert(name)
235 return util.pconvert(name)
238 elif name == root:
236 elif name == root:
239 return b''
237 return b''
240 else:
238 else:
241 # Determine whether `name' is in the hierarchy at or beneath `root',
239 # Determine whether `name' is in the hierarchy at or beneath `root',
242 # by iterating name=dirname(name) until that causes no change (can't
240 # by iterating name=dirname(name) until that causes no change (can't
243 # check name == '/', because that doesn't work on windows). The list
241 # check name == '/', because that doesn't work on windows). The list
244 # `rel' holds the reversed list of components making up the relative
242 # `rel' holds the reversed list of components making up the relative
245 # file name we want.
243 # file name we want.
246 rel = []
244 rel = []
247 while True:
245 while True:
248 try:
246 try:
249 s = util.samefile(name, root)
247 s = util.samefile(name, root)
250 except OSError:
248 except OSError:
251 s = False
249 s = False
252 if s:
250 if s:
253 if not rel:
251 if not rel:
254 # name was actually the same as root (maybe a symlink)
252 # name was actually the same as root (maybe a symlink)
255 return b''
253 return b''
256 rel.reverse()
254 rel.reverse()
257 name = os.path.join(*rel)
255 name = os.path.join(*rel)
258 auditor(name)
256 auditor(name)
259 return util.pconvert(name)
257 return util.pconvert(name)
260 dirname, basename = util.split(name)
258 dirname, basename = util.split(name)
261 rel.append(basename)
259 rel.append(basename)
262 if dirname == name:
260 if dirname == name:
263 break
261 break
264 name = dirname
262 name = dirname
265
263
266 # A common mistake is to use -R, but specify a file relative to the repo
264 # A common mistake is to use -R, but specify a file relative to the repo
267 # instead of cwd. Detect that case, and provide a hint to the user.
265 # instead of cwd. Detect that case, and provide a hint to the user.
268 hint = None
266 hint = None
269 try:
267 try:
270 if cwd != root:
268 if cwd != root:
271 canonpath(root, root, myname, auditor)
269 canonpath(root, root, myname, auditor)
272 relpath = util.pathto(root, cwd, b'')
270 relpath = util.pathto(root, cwd, b'')
273 if relpath.endswith(pycompat.ossep):
271 if relpath.endswith(pycompat.ossep):
274 relpath = relpath[:-1]
272 relpath = relpath[:-1]
275 hint = _(b"consider using '--cwd %s'") % relpath
273 hint = _(b"consider using '--cwd %s'") % relpath
276 except error.Abort:
274 except error.Abort:
277 pass
275 pass
278
276
279 raise error.Abort(
277 raise error.Abort(
280 _(b"%s not under root '%s'") % (myname, root), hint=hint
278 _(b"%s not under root '%s'") % (myname, root), hint=hint
281 )
279 )
282
280
283
281
284 def normasprefix(path):
282 def normasprefix(path):
285 # type: (bytes) -> bytes
283 # type: (bytes) -> bytes
286 """normalize the specified path as path prefix
284 """normalize the specified path as path prefix
287
285
288 Returned value can be used safely for "p.startswith(prefix)",
286 Returned value can be used safely for "p.startswith(prefix)",
289 "p[len(prefix):]", and so on.
287 "p[len(prefix):]", and so on.
290
288
291 For efficiency, this expects "path" argument to be already
289 For efficiency, this expects "path" argument to be already
292 normalized by "os.path.normpath", "os.path.realpath", and so on.
290 normalized by "os.path.normpath", "os.path.realpath", and so on.
293
291
294 See also issue3033 for detail about need of this function.
292 See also issue3033 for detail about need of this function.
295
293
296 >>> normasprefix(b'/foo/bar').replace(pycompat.ossep, b'/')
294 >>> normasprefix(b'/foo/bar').replace(pycompat.ossep, b'/')
297 '/foo/bar/'
295 '/foo/bar/'
298 >>> normasprefix(b'/').replace(pycompat.ossep, b'/')
296 >>> normasprefix(b'/').replace(pycompat.ossep, b'/')
299 '/'
297 '/'
300 """
298 """
301 d, p = os.path.splitdrive(path)
299 d, p = os.path.splitdrive(path)
302 if len(p) != len(pycompat.ossep):
300 if len(p) != len(pycompat.ossep):
303 return path + pycompat.ossep
301 return path + pycompat.ossep
304 else:
302 else:
305 return path
303 return path
306
304
307
305
308 def finddirs(path):
306 def finddirs(path):
309 # type: (bytes) -> Iterator[bytes]
307 # type: (bytes) -> Iterator[bytes]
310 pos = path.rfind(b'/')
308 pos = path.rfind(b'/')
311 while pos != -1:
309 while pos != -1:
312 yield path[:pos]
310 yield path[:pos]
313 pos = path.rfind(b'/', 0, pos)
311 pos = path.rfind(b'/', 0, pos)
314 yield b''
312 yield b''
315
313
316
314
317 def finddirs_rev_noroot(path: bytes) -> Iterator[bytes]:
315 def finddirs_rev_noroot(path: bytes) -> Iterator[bytes]:
318 pos = path.find(pycompat.ossep)
316 pos = path.find(pycompat.ossep)
319 while pos != -1:
317 while pos != -1:
320 yield path[:pos]
318 yield path[:pos]
321 pos = path.find(pycompat.ossep, pos + 1)
319 pos = path.find(pycompat.ossep, pos + 1)
322
320
323
321
324 class dirs:
322 class dirs:
325 '''a multiset of directory names from a set of file paths'''
323 '''a multiset of directory names from a set of file paths'''
326
324
327 def __init__(self, map, only_tracked=False):
325 def __init__(self, map, only_tracked=False):
328 """
326 """
329 a dict map indicates a dirstate while a list indicates a manifest
327 a dict map indicates a dirstate while a list indicates a manifest
330 """
328 """
331 self._dirs = {}
329 self._dirs = {}
332 addpath = self.addpath
330 addpath = self.addpath
333 if isinstance(map, dict) and only_tracked:
331 if isinstance(map, dict) and only_tracked:
334 for f, s in map.items():
332 for f, s in map.items():
335 if s.state != b'r':
333 if s.state != b'r':
336 addpath(f)
334 addpath(f)
337 elif only_tracked:
335 elif only_tracked:
338 msg = b"`only_tracked` is only supported with a dict source"
336 msg = b"`only_tracked` is only supported with a dict source"
339 raise error.ProgrammingError(msg)
337 raise error.ProgrammingError(msg)
340 else:
338 else:
341 for f in map:
339 for f in map:
342 addpath(f)
340 addpath(f)
343
341
344 def addpath(self, path):
342 def addpath(self, path):
345 # type: (bytes) -> None
343 # type: (bytes) -> None
346 dirs = self._dirs
344 dirs = self._dirs
347 for base in finddirs(path):
345 for base in finddirs(path):
348 if base.endswith(b'/'):
346 if base.endswith(b'/'):
349 raise ValueError(
347 raise ValueError(
350 "found invalid consecutive slashes in path: %r" % base
348 "found invalid consecutive slashes in path: %r" % base
351 )
349 )
352 if base in dirs:
350 if base in dirs:
353 dirs[base] += 1
351 dirs[base] += 1
354 return
352 return
355 dirs[base] = 1
353 dirs[base] = 1
356
354
357 def delpath(self, path):
355 def delpath(self, path):
358 # type: (bytes) -> None
356 # type: (bytes) -> None
359 dirs = self._dirs
357 dirs = self._dirs
360 for base in finddirs(path):
358 for base in finddirs(path):
361 if dirs[base] > 1:
359 if dirs[base] > 1:
362 dirs[base] -= 1
360 dirs[base] -= 1
363 return
361 return
364 del dirs[base]
362 del dirs[base]
365
363
366 def __iter__(self):
364 def __iter__(self):
367 return iter(self._dirs)
365 return iter(self._dirs)
368
366
369 def __contains__(self, d):
367 def __contains__(self, d):
370 # type: (bytes) -> bool
368 # type: (bytes) -> bool
371 return d in self._dirs
369 return d in self._dirs
372
370
373
371
374 if util.safehasattr(parsers, 'dirs'):
372 if util.safehasattr(parsers, 'dirs'):
375 dirs = parsers.dirs
373 dirs = parsers.dirs
376
374
377 if rustdirs is not None:
375 if rustdirs is not None:
378 dirs = rustdirs
376 dirs = rustdirs
379
377
380
378
381 # forward two methods from posixpath that do what we need, but we'd
379 # forward two methods from posixpath that do what we need, but we'd
382 # rather not let our internals know that we're thinking in posix terms
380 # rather not let our internals know that we're thinking in posix terms
383 # - instead we'll let them be oblivious.
381 # - instead we'll let them be oblivious.
384 join = posixpath.join
382 join = posixpath.join
385 dirname = posixpath.dirname # type: Callable[[bytes], bytes]
383 dirname = posixpath.dirname # type: Callable[[bytes], bytes]
General Comments 0
You need to be logged in to leave comments. Login now