dissect.target 3.14.dev29__py3-none-any.whl → 3.15__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. dissect/target/containers/ewf.py +1 -1
  2. dissect/target/containers/vhd.py +5 -2
  3. dissect/target/filesystem.py +36 -18
  4. dissect/target/filesystems/dir.py +10 -4
  5. dissect/target/filesystems/jffs.py +122 -0
  6. dissect/target/helpers/compat/path_310.py +506 -0
  7. dissect/target/helpers/compat/path_311.py +539 -0
  8. dissect/target/helpers/compat/path_312.py +443 -0
  9. dissect/target/helpers/compat/path_39.py +545 -0
  10. dissect/target/helpers/compat/path_common.py +223 -0
  11. dissect/target/helpers/cyber.py +512 -0
  12. dissect/target/helpers/fsutil.py +128 -666
  13. dissect/target/helpers/hashutil.py +17 -57
  14. dissect/target/helpers/keychain.py +9 -3
  15. dissect/target/helpers/loaderutil.py +1 -1
  16. dissect/target/helpers/mount.py +47 -4
  17. dissect/target/helpers/polypath.py +73 -0
  18. dissect/target/helpers/record_modifier.py +100 -0
  19. dissect/target/loader.py +2 -1
  20. dissect/target/loaders/asdf.py +2 -0
  21. dissect/target/loaders/cyber.py +37 -0
  22. dissect/target/loaders/log.py +14 -3
  23. dissect/target/loaders/raw.py +2 -0
  24. dissect/target/loaders/remote.py +12 -0
  25. dissect/target/loaders/tar.py +13 -0
  26. dissect/target/loaders/targetd.py +2 -0
  27. dissect/target/loaders/velociraptor.py +12 -3
  28. dissect/target/loaders/vmwarevm.py +2 -0
  29. dissect/target/plugin.py +272 -143
  30. dissect/target/plugins/apps/ssh/openssh.py +11 -54
  31. dissect/target/plugins/apps/ssh/opensshd.py +4 -3
  32. dissect/target/plugins/apps/ssh/putty.py +236 -0
  33. dissect/target/plugins/apps/ssh/ssh.py +58 -0
  34. dissect/target/plugins/apps/vpn/openvpn.py +6 -0
  35. dissect/target/plugins/apps/webserver/apache.py +309 -95
  36. dissect/target/plugins/apps/webserver/caddy.py +5 -2
  37. dissect/target/plugins/apps/webserver/citrix.py +82 -0
  38. dissect/target/plugins/apps/webserver/iis.py +9 -12
  39. dissect/target/plugins/apps/webserver/nginx.py +5 -2
  40. dissect/target/plugins/apps/webserver/webserver.py +25 -41
  41. dissect/target/plugins/child/wsl.py +1 -1
  42. dissect/target/plugins/filesystem/ntfs/mft.py +10 -0
  43. dissect/target/plugins/filesystem/ntfs/mft_timeline.py +10 -0
  44. dissect/target/plugins/filesystem/ntfs/usnjrnl.py +10 -0
  45. dissect/target/plugins/filesystem/ntfs/utils.py +28 -5
  46. dissect/target/plugins/filesystem/resolver.py +6 -4
  47. dissect/target/plugins/general/default.py +0 -2
  48. dissect/target/plugins/general/example.py +0 -1
  49. dissect/target/plugins/general/loaders.py +3 -5
  50. dissect/target/plugins/os/unix/_os.py +3 -3
  51. dissect/target/plugins/os/unix/bsd/citrix/_os.py +68 -28
  52. dissect/target/plugins/os/unix/bsd/citrix/history.py +130 -0
  53. dissect/target/plugins/os/unix/generic.py +17 -12
  54. dissect/target/plugins/os/unix/linux/fortios/__init__.py +0 -0
  55. dissect/target/plugins/os/unix/linux/fortios/_os.py +534 -0
  56. dissect/target/plugins/os/unix/linux/fortios/generic.py +30 -0
  57. dissect/target/plugins/os/unix/linux/fortios/locale.py +109 -0
  58. dissect/target/plugins/os/windows/log/evt.py +1 -1
  59. dissect/target/plugins/os/windows/log/schedlgu.py +155 -0
  60. dissect/target/plugins/os/windows/regf/firewall.py +1 -1
  61. dissect/target/plugins/os/windows/regf/shimcache.py +1 -1
  62. dissect/target/plugins/os/windows/regf/trusteddocs.py +1 -1
  63. dissect/target/plugins/os/windows/registry.py +1 -1
  64. dissect/target/plugins/os/windows/sam.py +3 -0
  65. dissect/target/plugins/os/windows/sru.py +41 -28
  66. dissect/target/plugins/os/windows/tasks.py +5 -2
  67. dissect/target/target.py +7 -3
  68. dissect/target/tools/dd.py +7 -1
  69. dissect/target/tools/fs.py +8 -1
  70. dissect/target/tools/info.py +22 -16
  71. dissect/target/tools/mount.py +28 -3
  72. dissect/target/tools/query.py +146 -117
  73. dissect/target/tools/reg.py +21 -16
  74. dissect/target/tools/shell.py +30 -6
  75. dissect/target/tools/utils.py +28 -0
  76. dissect/target/volumes/bde.py +14 -10
  77. dissect/target/volumes/luks.py +18 -10
  78. {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/METADATA +4 -3
  79. {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/RECORD +85 -67
  80. dissect/target/plugins/os/unix/linux/fortigate/_os.py +0 -175
  81. /dissect/target/{plugins/os/unix/linux/fortigate → helpers/compat}/__init__.py +0 -0
  82. {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/COPYRIGHT +0 -0
  83. {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/LICENSE +0 -0
  84. {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/WHEEL +0 -0
  85. {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/entry_points.txt +0 -0
  86. {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,506 @@
1
+ """A pathlib.Path compatible implementation for dissect.target.
2
+
3
+ This allows for the majority of the pathlib.Path API to "just work" on dissect.target filesystems.
4
+
5
+ Most of this consists of subclassed internal classes with dissect.target specific patches,
6
+ but sometimes the change to a function is small, so the entire internal function is copied
7
+ and only a small part changed. To ease updating this code, the order of functions, comments
8
+ and code style is kept largely the same as the original pathlib.py.
9
+
10
+ Yes, we know, this is playing with fire and it can break on new CPython releases.
11
+
12
+ The implementation is split up in multiple files, one for each CPython version.
13
+ You're currently looking at the CPython 3.10 implementation.
14
+
15
+ Commit hash we're in sync with: b382bf5
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import fnmatch
21
+ import re
22
+ from pathlib import Path, PurePath, _Accessor, _PosixFlavour
23
+ from stat import S_ISBLK, S_ISCHR, S_ISFIFO, S_ISSOCK
24
+ from typing import IO, TYPE_CHECKING, Any, Callable, Iterator, Optional
25
+
26
+ from dissect.target import filesystem
27
+ from dissect.target.exceptions import FilesystemError, SymlinkRecursionError
28
+ from dissect.target.helpers.compat.path_common import (
29
+ _DissectPathParents,
30
+ io_open,
31
+ isjunction,
32
+ realpath,
33
+ scandir,
34
+ )
35
+ from dissect.target.helpers.polypath import normalize
36
+
37
+ if TYPE_CHECKING:
38
+ from dissect.target.filesystem import Filesystem, FilesystemEntry
39
+ from dissect.target.helpers.compat.path_common import _DissectScandirIterator
40
+ from dissect.target.helpers.fsutil import stat_result
41
+
42
+
43
+ class _DissectFlavour(_PosixFlavour):
44
+ is_supported = True
45
+
46
+ __variant_instances = {}
47
+
48
+ def __new__(cls, case_sensitive: bool = False, alt_separator: str = ""):
49
+ idx = (case_sensitive, alt_separator)
50
+ instance = cls.__variant_instances.get(idx, None)
51
+ if instance is None:
52
+ instance = _PosixFlavour.__new__(cls)
53
+ cls.__variant_instances[idx] = instance
54
+
55
+ return instance
56
+
57
+ def __init__(self, case_sensitive: bool = False, alt_separator: str = ""):
58
+ super().__init__()
59
+ self.altsep = alt_separator
60
+ self.case_sensitive = case_sensitive
61
+
62
+ def casefold(self, s: str) -> str:
63
+ return s if self.case_sensitive else s.lower()
64
+
65
+ def casefold_parts(self, parts: list[str]) -> list[str]:
66
+ return parts if self.case_sensitive else [p.lower() for p in parts]
67
+
68
+ def compile_pattern(self, pattern: str) -> Callable[..., Any]:
69
+ return re.compile(fnmatch.translate(pattern), 0 if self.case_sensitive else re.IGNORECASE).fullmatch
70
+
71
+
72
+ class _DissectAccessor(_Accessor):
73
+ @staticmethod
74
+ def stat(path: TargetPath, *, follow_symlinks: bool = True) -> stat_result:
75
+ if follow_symlinks:
76
+ return path.get().stat()
77
+ else:
78
+ return path.get().lstat()
79
+
80
+ @staticmethod
81
+ def open(
82
+ path: TargetPath,
83
+ mode: str = "rb",
84
+ buffering: int = 0,
85
+ encoding: Optional[str] = None,
86
+ errors: Optional[str] = None,
87
+ newline: Optional[str] = None,
88
+ ) -> IO:
89
+ """Open file and return a stream.
90
+
91
+ Supports a subset of features of the real pathlib.open/io.open.
92
+
93
+ Note: in contrast to regular Python, the mode is binary by default. Text mode
94
+ has to be explicitly specified. Buffering is also disabled by default.
95
+ """
96
+ return io_open(path, mode, buffering, encoding, errors, newline)
97
+
98
+ @staticmethod
99
+ def listdir(path: TargetPath) -> Iterator[str]:
100
+ return path.get().listdir()
101
+
102
+ @staticmethod
103
+ def scandir(path: TargetPath) -> _DissectScandirIterator:
104
+ return scandir(path)
105
+
106
+ @staticmethod
107
+ def chmod(path: TargetPath, mode: int, *, follow_symlinks: bool = True) -> None:
108
+ raise NotImplementedError("TargetPath.chmod() is unsupported")
109
+
110
+ @staticmethod
111
+ def lchmod(path: TargetPath, mode: int) -> None:
112
+ raise NotImplementedError("TargetPath.lchmod() is unsupported")
113
+
114
+ @staticmethod
115
+ def mkdir(path: TargetPath, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None:
116
+ raise NotImplementedError("TargetPath.mkdir() is unsupported")
117
+
118
+ @staticmethod
119
+ def unlink(path: TargetPath, missing_ok: bool = False) -> None:
120
+ raise NotImplementedError("TargetPath.unlink() is unsupported")
121
+
122
+ @staticmethod
123
+ def link(src: str, dst: TargetPath) -> None:
124
+ raise NotImplementedError("TargetPath.link() is unsupported")
125
+
126
+ @staticmethod
127
+ def rmdir(path: TargetPath) -> None:
128
+ raise NotImplementedError("TargetPath.rmdir() is unsupported")
129
+
130
+ @staticmethod
131
+ def rename(path: TargetPath, target: str) -> str:
132
+ raise NotImplementedError("TargetPath.rename() is unsupported")
133
+
134
+ @staticmethod
135
+ def replace(path: TargetPath, target: str) -> str:
136
+ raise NotImplementedError("TargetPath.replace() is unsupported")
137
+
138
+ @staticmethod
139
+ def symlink(path: TargetPath, target: str, target_is_directory: bool = False) -> None:
140
+ raise NotImplementedError("TargetPath.symlink() is unsupported")
141
+
142
+ @staticmethod
143
+ def touch(path: TargetPath, mode: int = 0o666, exist_ok: bool = True) -> None:
144
+ raise NotImplementedError("TargetPath.touch() is unsupported")
145
+
146
+ @staticmethod
147
+ def readlink(path: TargetPath) -> str:
148
+ return path.get().readlink()
149
+
150
+ @staticmethod
151
+ def owner(path: TargetPath) -> str:
152
+ raise NotImplementedError("TargetPath.owner() is unsupported")
153
+
154
+ @staticmethod
155
+ def group(path: TargetPath) -> str:
156
+ raise NotImplementedError("TargetPath.group() is unsupported")
157
+
158
+ @staticmethod
159
+ def getcwd() -> str:
160
+ raise NotImplementedError("TargetPath.getcwd() is unsupported")
161
+
162
+ @staticmethod
163
+ def expanduser(path: str) -> str:
164
+ raise NotImplementedError("TargetPath.expanduser() is unsupported")
165
+
166
+ realpath = staticmethod(realpath)
167
+
168
+ # NOTE: Forward compatibility with CPython >= 3.12
169
+ isjunction = staticmethod(isjunction)
170
+
171
+
172
+ _dissect_accessor = _DissectAccessor()
173
+
174
+
175
+ class PureDissectPath(PurePath):
176
+ _fs: Filesystem
177
+ _flavour = _DissectFlavour(case_sensitive=False)
178
+
179
+ def __reduce__(self) -> tuple:
180
+ raise TypeError("TargetPath pickling is currently not supported")
181
+
182
+ @classmethod
183
+ def _from_parts(cls, args: list) -> TargetPath:
184
+ fs = args[0]
185
+
186
+ if not isinstance(fs, filesystem.Filesystem):
187
+ raise TypeError(
188
+ "invalid PureDissectPath initialization: missing filesystem, "
189
+ "got %r (this might be a bug, please report)" % args
190
+ )
191
+
192
+ alt_separator = fs.alt_separator
193
+ path_args = []
194
+ for arg in args[1:]:
195
+ if isinstance(arg, str):
196
+ arg = normalize(arg, alt_separator=alt_separator)
197
+ path_args.append(arg)
198
+
199
+ self = super()._from_parts(path_args)
200
+ self._fs = fs
201
+
202
+ self._flavour = _DissectFlavour(alt_separator=fs.alt_separator, case_sensitive=fs.case_sensitive)
203
+
204
+ return self
205
+
206
+ def _make_child(self, args: list) -> TargetPath:
207
+ child = super()._make_child(args)
208
+ child._fs = self._fs
209
+ child._flavour = self._flavour
210
+ return child
211
+
212
+ def with_name(self, name: str) -> TargetPath:
213
+ result = super().with_name(name)
214
+ result._fs = self._fs
215
+ result._flavour = self._flavour
216
+ return result
217
+
218
+ def with_stem(self, stem: str) -> TargetPath:
219
+ result = super().with_stem(stem)
220
+ result._fs = self._fs
221
+ result._flavour = self._flavour
222
+ return result
223
+
224
+ def with_suffix(self, suffix: str) -> TargetPath:
225
+ result = super().with_suffix(suffix)
226
+ result._fs = self._fs
227
+ result._flavour = self._flavour
228
+ return result
229
+
230
+ def relative_to(self, *other) -> TargetPath:
231
+ result = super().relative_to(*other)
232
+ result._fs = self._fs
233
+ result._flavour = self._flavour
234
+ return result
235
+
236
+ def __rtruediv__(self, key: str) -> TargetPath:
237
+ try:
238
+ return self._from_parts([self._fs, key] + self._parts)
239
+ except TypeError:
240
+ return NotImplemented
241
+
242
+ @property
243
+ def parent(self) -> TargetPath:
244
+ result = super().parent
245
+ result._fs = self._fs
246
+ result._flavour = self._flavour
247
+ return result
248
+
249
+ @property
250
+ def parents(self) -> _DissectPathParents:
251
+ return _DissectPathParents(self)
252
+
253
+
254
+ class TargetPath(Path, PureDissectPath):
255
+ _accessor = _dissect_accessor
256
+ __slots__ = ("_entry",)
257
+
258
+ def _make_child_relpath(self, part: str) -> TargetPath:
259
+ child = super()._make_child_relpath(part)
260
+ child._fs = self._fs
261
+ child._flavour = self._flavour
262
+ return child
263
+
264
+ def get(self) -> FilesystemEntry:
265
+ try:
266
+ return self._entry
267
+ except AttributeError:
268
+ self._entry = self._fs.get(str(self))
269
+ return self._entry
270
+
271
+ @classmethod
272
+ def cwd(cls) -> TargetPath:
273
+ """Return a new path pointing to the current working directory
274
+ (as returned by os.getcwd()).
275
+ """
276
+ raise NotImplementedError("TargetPath.cwd() is unsupported")
277
+
278
+ @classmethod
279
+ def home(cls) -> TargetPath:
280
+ """Return a new path pointing to the user's home directory (as
281
+ returned by os.path.expanduser('~')).
282
+ """
283
+ raise NotImplementedError("TargetPath.home() is unsupported")
284
+
285
+ def iterdir(self) -> Iterator[TargetPath]:
286
+ """Iterate over the files in this directory. Does not yield any
287
+ result for the special paths '.' and '..'.
288
+ """
289
+ for entry in self._accessor.scandir(self):
290
+ if entry.name in {".", ".."}:
291
+ # Yielding a path object for these makes little sense
292
+ continue
293
+ child_path = self._make_child_relpath(entry.name)
294
+ child_path._entry = entry
295
+ yield child_path
296
+
297
+ # NOTE: Forward compatibility with CPython >= 3.12
298
+ def walk(
299
+ self, top_down: bool = True, on_error: Callable[[Exception], None] = None, follow_symlinks: bool = False
300
+ ) -> Iterator[tuple[TargetPath, list[str], list[str]]]:
301
+ """Walk the directory tree from this directory, similar to os.walk()."""
302
+ paths = [self]
303
+
304
+ while paths:
305
+ path = paths.pop()
306
+ if isinstance(path, tuple):
307
+ yield path
308
+ continue
309
+
310
+ # We may not have read permission for self, in which case we can't
311
+ # get a list of the files the directory contains. os.walk()
312
+ # always suppressed the exception in that instance, rather than
313
+ # blow up for a minor reason when (say) a thousand readable
314
+ # directories are still left to visit. That logic is copied here.
315
+ try:
316
+ scandir_it = self._accessor.scandir(path)
317
+ except OSError as error:
318
+ if on_error is not None:
319
+ on_error(error)
320
+ continue
321
+
322
+ with scandir_it:
323
+ dirnames = []
324
+ filenames = []
325
+ for entry in scandir_it:
326
+ try:
327
+ is_dir = entry.is_dir(follow_symlinks=follow_symlinks)
328
+ except OSError:
329
+ # Carried over from os.path.isdir().
330
+ is_dir = False
331
+
332
+ if is_dir:
333
+ dirnames.append(entry.name)
334
+ else:
335
+ filenames.append(entry.name)
336
+
337
+ if top_down:
338
+ yield path, dirnames, filenames
339
+ else:
340
+ paths.append((path, dirnames, filenames))
341
+
342
+ paths += [path._make_child_relpath(d) for d in reversed(dirnames)]
343
+
344
+ def absolute(self) -> TargetPath:
345
+ """Return an absolute version of this path. This function works
346
+ even if the path doesn't point to anything.
347
+
348
+ No normalization is done, i.e. all '.' and '..' will be kept along.
349
+ Use resolve() to get the canonical path to a file.
350
+ """
351
+ raise NotImplementedError("TargetPath.absolute() is unsupported in Dissect")
352
+
353
+ # NOTE: We changed some of the error handling here to deal with our own exception types
354
+ def resolve(self, strict: bool = False) -> TargetPath:
355
+ """
356
+ Make the path absolute, resolving all symlinks on the way and also
357
+ normalizing it (for example turning slashes into backslashes under
358
+ Windows).
359
+ """
360
+
361
+ s = self._accessor.realpath(self, strict=strict)
362
+ p = self._from_parts((self._fs, s))
363
+
364
+ # In non-strict mode, realpath() doesn't raise on symlink loops.
365
+ # Ensure we get an exception by calling stat()
366
+ if not strict:
367
+ try:
368
+ p.stat()
369
+ except FilesystemError as e:
370
+ if isinstance(e, SymlinkRecursionError):
371
+ raise
372
+ return p
373
+
374
+ def stat(self, *, follow_symlinks: bool = True) -> stat_result:
375
+ """
376
+ Return the result of the stat() system call on this path, like
377
+ os.stat() does.
378
+ """
379
+ return self._accessor.stat(self, follow_symlinks=follow_symlinks)
380
+
381
+ def open(
382
+ self,
383
+ mode: str = "rb",
384
+ buffering: int = 0,
385
+ encoding: Optional[str] = None,
386
+ errors: Optional[str] = None,
387
+ newline: Optional[str] = None,
388
+ ) -> IO:
389
+ """Open file and return a stream.
390
+
391
+ Supports a subset of features of the real pathlib.open/io.open.
392
+
393
+ Note: in contrast to regular Python, the mode is binary by default. Text mode
394
+ has to be explicitly specified. Buffering is also disabled by default.
395
+ """
396
+ return self._accessor.open(self, mode, buffering, encoding, errors, newline)
397
+
398
+ def write_bytes(self, data: bytes) -> int:
399
+ """
400
+ Open the file in bytes mode, write to it, and close the file.
401
+ """
402
+ raise NotImplementedError("TargetPath.write_bytes() is unsupported")
403
+
404
+ def write_text(
405
+ self, data: str, encoding: Optional[str] = None, errors: Optional[str] = None, newline: Optional[str] = None
406
+ ) -> int:
407
+ """
408
+ Open the file in text mode, write to it, and close the file.
409
+ """
410
+ raise NotImplementedError("TargetPath.write_text() is unsupported")
411
+
412
+ def readlink(self) -> TargetPath:
413
+ """
414
+ Return the path to which the symbolic link points.
415
+ """
416
+ path = self._accessor.readlink(self)
417
+ obj = self._from_parts((self._fs, path))
418
+ return obj
419
+
420
+ def exists(self) -> bool:
421
+ """
422
+ Whether this path exists.
423
+ """
424
+ try:
425
+ # .exists() must resolve possible symlinks
426
+ self.get().stat()
427
+ return True
428
+ except (FilesystemError, ValueError):
429
+ return False
430
+
431
+ def is_dir(self) -> bool:
432
+ """
433
+ Whether this path is a directory.
434
+ """
435
+ try:
436
+ return self.get().is_dir()
437
+ except (FilesystemError, ValueError):
438
+ return False
439
+
440
+ def is_file(self) -> bool:
441
+ """
442
+ Whether this path is a regular file (also True for symlinks pointing
443
+ to regular files).
444
+ """
445
+ try:
446
+ return self.get().is_file()
447
+ except (FilesystemError, ValueError):
448
+ return False
449
+
450
+ def is_symlink(self) -> bool:
451
+ """
452
+ Whether this path is a symbolic link.
453
+ """
454
+ try:
455
+ return self.get().is_symlink()
456
+ except (FilesystemError, ValueError):
457
+ return False
458
+
459
+ # NOTE: Forward compatibility with CPython >= 3.12
460
+ def is_junction(self) -> bool:
461
+ """
462
+ Whether this path is a junction.
463
+ """
464
+ return self._accessor.isjunction(self)
465
+
466
+ def is_block_device(self) -> bool:
467
+ """
468
+ Whether this path is a block device.
469
+ """
470
+ try:
471
+ return S_ISBLK(self.stat().st_mode)
472
+ except (FilesystemError, ValueError):
473
+ return False
474
+
475
+ def is_char_device(self) -> bool:
476
+ """
477
+ Whether this path is a character device.
478
+ """
479
+ try:
480
+ return S_ISCHR(self.stat().st_mode)
481
+ except (FilesystemError, ValueError):
482
+ return False
483
+
484
+ def is_fifo(self) -> bool:
485
+ """
486
+ Whether this path is a FIFO.
487
+ """
488
+ try:
489
+ return S_ISFIFO(self.stat().st_mode)
490
+ except (FilesystemError, ValueError):
491
+ return False
492
+
493
+ def is_socket(self) -> bool:
494
+ """
495
+ Whether this path is a socket.
496
+ """
497
+ try:
498
+ return S_ISSOCK(self.stat().st_mode)
499
+ except (FilesystemError, ValueError):
500
+ return False
501
+
502
+ def expanduser(self) -> TargetPath:
503
+ """Return a new path with expanded ~ and ~user constructs
504
+ (as returned by os.path.expanduser)
505
+ """
506
+ raise NotImplementedError("TargetPath.expanduser() is unsupported")