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,539 @@
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.11 implementation.
14
+
15
+ Commit hash we're in sync with: 846a23d
16
+
17
+ Notes:
18
+ - CPython 3.11 ditched the _Accessor class, so we override the methods that should use it
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import fnmatch
23
+ import re
24
+ from pathlib import Path, PurePath, _PosixFlavour
25
+ from stat import S_ISBLK, S_ISCHR, S_ISFIFO, S_ISSOCK
26
+ from typing import IO, TYPE_CHECKING, Any, Callable, Iterator, Optional
27
+
28
+ from dissect.target import filesystem
29
+ from dissect.target.exceptions import (
30
+ FileNotFoundError,
31
+ FilesystemError,
32
+ NotADirectoryError,
33
+ NotASymlinkError,
34
+ SymlinkRecursionError,
35
+ )
36
+ from dissect.target.helpers.compat.path_common import (
37
+ _DissectPathParents,
38
+ io_open,
39
+ isjunction,
40
+ realpath,
41
+ scandir,
42
+ )
43
+ from dissect.target.helpers.polypath import normalize
44
+
45
+ if TYPE_CHECKING:
46
+ from dissect.target.filesystem import Filesystem, FilesystemEntry
47
+ from dissect.target.helpers.compat.path_common import _DissectScandirIterator
48
+ from dissect.target.helpers.fsutil import stat_result
49
+
50
+
51
+ class _DissectFlavour(_PosixFlavour):
52
+ is_supported = True
53
+
54
+ __variant_instances = {}
55
+
56
+ def __new__(cls, case_sensitive: bool = False, alt_separator: str = ""):
57
+ idx = (case_sensitive, alt_separator)
58
+ instance = cls.__variant_instances.get(idx, None)
59
+ if instance is None:
60
+ instance = _PosixFlavour.__new__(cls)
61
+ cls.__variant_instances[idx] = instance
62
+
63
+ return instance
64
+
65
+ def __init__(self, case_sensitive: bool = False, alt_separator: str = ""):
66
+ super().__init__()
67
+ self.altsep = alt_separator
68
+ self.case_sensitive = case_sensitive
69
+
70
+ def casefold(self, s: str) -> str:
71
+ return s if self.case_sensitive else s.lower()
72
+
73
+ def casefold_parts(self, parts: list[str]) -> list[str]:
74
+ return parts if self.case_sensitive else [p.lower() for p in parts]
75
+
76
+ def compile_pattern(self, pattern: str) -> Callable[..., Any]:
77
+ return re.compile(fnmatch.translate(pattern), 0 if self.case_sensitive else re.IGNORECASE).fullmatch
78
+
79
+ def is_reserved(self, parts: list[str]) -> bool:
80
+ return False
81
+
82
+
83
+ class PureDissectPath(PurePath):
84
+ _fs: Filesystem
85
+ _flavour = _DissectFlavour(case_sensitive=False)
86
+
87
+ def __reduce__(self) -> tuple:
88
+ raise TypeError("TargetPath pickling is currently not supported")
89
+
90
+ @classmethod
91
+ def _from_parts(cls, args: list) -> TargetPath:
92
+ fs = args[0]
93
+
94
+ if not isinstance(fs, filesystem.Filesystem):
95
+ raise TypeError(
96
+ "invalid PureDissectPath initialization: missing filesystem, "
97
+ "got %r (this might be a bug, please report)" % args
98
+ )
99
+
100
+ alt_separator = fs.alt_separator
101
+ path_args = []
102
+ for arg in args[1:]:
103
+ if isinstance(arg, str):
104
+ arg = normalize(arg, alt_separator=alt_separator)
105
+ path_args.append(arg)
106
+
107
+ self = super()._from_parts(path_args)
108
+ self._fs = fs
109
+
110
+ self._flavour = _DissectFlavour(alt_separator=fs.alt_separator, case_sensitive=fs.case_sensitive)
111
+
112
+ return self
113
+
114
+ def _make_child(self, args: list) -> TargetPath:
115
+ child = super()._make_child(args)
116
+ child._fs = self._fs
117
+ child._flavour = self._flavour
118
+ return child
119
+
120
+ def with_name(self, name: str) -> TargetPath:
121
+ result = super().with_name(name)
122
+ result._fs = self._fs
123
+ result._flavour = self._flavour
124
+ return result
125
+
126
+ def with_stem(self, stem: str) -> TargetPath:
127
+ result = super().with_stem(stem)
128
+ result._fs = self._fs
129
+ result._flavour = self._flavour
130
+ return result
131
+
132
+ def with_suffix(self, suffix: str) -> TargetPath:
133
+ result = super().with_suffix(suffix)
134
+ result._fs = self._fs
135
+ result._flavour = self._flavour
136
+ return result
137
+
138
+ def relative_to(self, *other) -> TargetPath:
139
+ result = super().relative_to(*other)
140
+ result._fs = self._fs
141
+ result._flavour = self._flavour
142
+ return result
143
+
144
+ def __rtruediv__(self, key: str) -> TargetPath:
145
+ try:
146
+ return self._from_parts([self._fs, key] + self._parts)
147
+ except TypeError:
148
+ return NotImplemented
149
+
150
+ @property
151
+ def parent(self) -> TargetPath:
152
+ result = super().parent
153
+ result._fs = self._fs
154
+ result._flavour = self._flavour
155
+ return result
156
+
157
+ @property
158
+ def parents(self) -> _DissectPathParents:
159
+ return _DissectPathParents(self)
160
+
161
+
162
+ class TargetPath(Path, PureDissectPath):
163
+ __slots__ = ("_entry",)
164
+
165
+ def _make_child_relpath(self, part: str) -> TargetPath:
166
+ child = super()._make_child_relpath(part)
167
+ child._fs = self._fs
168
+ child._flavour = self._flavour
169
+ return child
170
+
171
+ def get(self) -> FilesystemEntry:
172
+ try:
173
+ return self._entry
174
+ except AttributeError:
175
+ self._entry = self._fs.get(str(self))
176
+ return self._entry
177
+
178
+ @classmethod
179
+ def cwd(cls) -> TargetPath:
180
+ """Return a new path pointing to the current working directory
181
+ (as returned by os.getcwd()).
182
+ """
183
+ raise NotImplementedError("TargetPath.cwd() is unsupported")
184
+
185
+ @classmethod
186
+ def home(cls) -> TargetPath:
187
+ """Return a new path pointing to the user's home directory (as
188
+ returned by os.path.expanduser('~')).
189
+ """
190
+ raise NotImplementedError("TargetPath.home() is unsupported")
191
+
192
+ def iterdir(self) -> Iterator[TargetPath]:
193
+ """Iterate over the files in this directory. Does not yield any
194
+ result for the special paths '.' and '..'.
195
+ """
196
+ for entry in scandir(self):
197
+ if entry.name in {".", ".."}:
198
+ # Yielding a path object for these makes little sense
199
+ continue
200
+ child_path = self._make_child_relpath(entry.name)
201
+ child_path._entry = entry
202
+ yield child_path
203
+
204
+ def _scandir(self) -> _DissectScandirIterator:
205
+ return scandir(self)
206
+
207
+ # NOTE: Forward compatibility with CPython >= 3.12
208
+ def walk(
209
+ self, top_down: bool = True, on_error: Callable[[Exception], None] = None, follow_symlinks: bool = False
210
+ ) -> Iterator[tuple[TargetPath, list[str], list[str]]]:
211
+ """Walk the directory tree from this directory, similar to os.walk()."""
212
+ paths = [self]
213
+
214
+ while paths:
215
+ path = paths.pop()
216
+ if isinstance(path, tuple):
217
+ yield path
218
+ continue
219
+
220
+ # We may not have read permission for self, in which case we can't
221
+ # get a list of the files the directory contains. os.walk()
222
+ # always suppressed the exception in that instance, rather than
223
+ # blow up for a minor reason when (say) a thousand readable
224
+ # directories are still left to visit. That logic is copied here.
225
+ try:
226
+ scandir_it = path._scandir()
227
+ except OSError as error:
228
+ if on_error is not None:
229
+ on_error(error)
230
+ continue
231
+
232
+ with scandir_it:
233
+ dirnames = []
234
+ filenames = []
235
+ for entry in scandir_it:
236
+ try:
237
+ is_dir = entry.is_dir(follow_symlinks=follow_symlinks)
238
+ except OSError:
239
+ # Carried over from os.path.isdir().
240
+ is_dir = False
241
+
242
+ if is_dir:
243
+ dirnames.append(entry.name)
244
+ else:
245
+ filenames.append(entry.name)
246
+
247
+ if top_down:
248
+ yield path, dirnames, filenames
249
+ else:
250
+ paths.append((path, dirnames, filenames))
251
+
252
+ paths += [path._make_child_relpath(d) for d in reversed(dirnames)]
253
+
254
+ def absolute(self) -> TargetPath:
255
+ """Return an absolute version of this path. This function works
256
+ even if the path doesn't point to anything.
257
+
258
+ No normalization is done, i.e. all '.' and '..' will be kept along.
259
+ Use resolve() to get the canonical path to a file.
260
+ """
261
+ raise NotImplementedError("TargetPath.absolute() is unsupported in Dissect")
262
+
263
+ # NOTE: We changed some of the error handling here to deal with our own exception types
264
+ def resolve(self, strict: bool = False) -> TargetPath:
265
+ """
266
+ Make the path absolute, resolving all symlinks on the way and also
267
+ normalizing it.
268
+ """
269
+
270
+ s = realpath(self, strict=strict)
271
+ p = self._from_parts((self._fs, s))
272
+
273
+ # In non-strict mode, realpath() doesn't raise on symlink loops.
274
+ # Ensure we get an exception by calling stat()
275
+ if not strict:
276
+ try:
277
+ p.stat()
278
+ except FilesystemError as e:
279
+ if isinstance(e, SymlinkRecursionError):
280
+ raise
281
+ return p
282
+
283
+ def stat(self, *, follow_symlinks: bool = True) -> stat_result:
284
+ """
285
+ Return the result of the stat() system call on this path, like
286
+ os.stat() does.
287
+ """
288
+ if follow_symlinks:
289
+ return self.get().stat()
290
+ else:
291
+ return self.get().lstat()
292
+
293
+ def owner(self) -> str:
294
+ """
295
+ Return the login name of the file owner.
296
+ """
297
+ raise NotImplementedError("TargetPath.owner() is unsupported")
298
+
299
+ def group(self) -> str:
300
+ """
301
+ Return the group name of the file gid.
302
+ """
303
+ raise NotImplementedError("TargetPath.group() is unsupported")
304
+
305
+ def open(
306
+ self,
307
+ mode: str = "rb",
308
+ buffering: int = 0,
309
+ encoding: Optional[str] = None,
310
+ errors: Optional[str] = None,
311
+ newline: Optional[str] = None,
312
+ ) -> IO:
313
+ """Open file and return a stream.
314
+
315
+ Supports a subset of features of the real pathlib.open/io.open.
316
+
317
+ Note: in contrast to regular Python, the mode is binary by default. Text mode
318
+ has to be explicitly specified. Buffering is also disabled by default.
319
+ """
320
+ return io_open(self, mode, buffering, encoding, errors, newline)
321
+
322
+ def write_bytes(self, data: bytes) -> int:
323
+ """
324
+ Open the file in bytes mode, write to it, and close the file.
325
+ """
326
+ raise NotImplementedError("TargetPath.write_bytes() is unsupported")
327
+
328
+ def write_text(
329
+ self, data: str, encoding: Optional[str] = None, errors: Optional[str] = None, newline: Optional[str] = None
330
+ ) -> int:
331
+ """
332
+ Open the file in text mode, write to it, and close the file.
333
+ """
334
+ raise NotImplementedError("TargetPath.write_text() is unsupported")
335
+
336
+ def readlink(self) -> TargetPath:
337
+ """
338
+ Return the path to which the symbolic link points.
339
+ """
340
+ return self._from_parts((self._fs, self.get().readlink()))
341
+
342
+ def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None:
343
+ """
344
+ Create this file with the given access mode, if it doesn't exist.
345
+ """
346
+ raise NotImplementedError("TargetPath.touch() is unsupported")
347
+
348
+ def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None:
349
+ """
350
+ Create a new directory at this given path.
351
+ """
352
+ raise NotImplementedError("TargetPath.mkdir() is unsupported")
353
+
354
+ def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None:
355
+ """
356
+ Change the permissions of the path, like os.chmod().
357
+ """
358
+ raise NotImplementedError("TargetPath.chmod() is unsupported")
359
+
360
+ def lchmod(self, mode: int) -> None:
361
+ """
362
+ Like chmod(), except if the path points to a symlink, the symlink's
363
+ permissions are changed, rather than its target's.
364
+ """
365
+ raise NotImplementedError("TargetPath.lchmod() is unsupported")
366
+
367
+ def unlink(self, missing_ok: bool = False) -> None:
368
+ """
369
+ Remove this file or link.
370
+ If the path is a directory, use rmdir() instead.
371
+ """
372
+ raise NotImplementedError("TargetPath.unlink() is unsupported")
373
+
374
+ def rmdir(self) -> None:
375
+ """
376
+ Remove this directory. The directory must be empty.
377
+ """
378
+ raise NotImplementedError("TargetPath.rmdir() is unsupported")
379
+
380
+ def rename(self, target: str) -> TargetPath:
381
+ """
382
+ Rename this path to the target path.
383
+
384
+ The target path may be absolute or relative. Relative paths are
385
+ interpreted relative to the current working directory, *not* the
386
+ directory of the Path object.
387
+
388
+ Returns the new Path instance pointing to the target path.
389
+ """
390
+ raise NotImplementedError("TargetPath.rename() is unsupported")
391
+
392
+ def replace(self, target: str) -> TargetPath:
393
+ """
394
+ Rename this path to the target path, overwriting if that path exists.
395
+
396
+ The target path may be absolute or relative. Relative paths are
397
+ interpreted relative to the current working directory, *not* the
398
+ directory of the Path object.
399
+
400
+ Returns the new Path instance pointing to the target path.
401
+ """
402
+ raise NotImplementedError("TargetPath.replace() is unsupported")
403
+
404
+ def symlink_to(self, target: str, target_is_directory: bool = False) -> None:
405
+ """
406
+ Make this path a symlink pointing to the target path.
407
+ Note the order of arguments (link, target) is the reverse of os.symlink.
408
+ """
409
+ raise NotImplementedError("TargetPath.symlink_to() is unsupported")
410
+
411
+ def hardlink_to(self, target: str) -> None:
412
+ """
413
+ Make this path a hard link pointing to the same file as *target*.
414
+
415
+ Note the order of arguments (self, target) is the reverse of os.link's.
416
+ """
417
+ raise NotImplementedError("TargetPath.hardlink_to() is unsupported")
418
+
419
+ def link_to(self, target: str) -> None:
420
+ """
421
+ Make the target path a hard link pointing to this path.
422
+
423
+ Note this function does not make this path a hard link to *target*,
424
+ despite the implication of the function and argument names. The order
425
+ of arguments (target, link) is the reverse of Path.symlink_to, but
426
+ matches that of os.link.
427
+
428
+ Deprecated since Python 3.10 and scheduled for removal in Python 3.12.
429
+ Use `hardlink_to()` instead.
430
+ """
431
+ raise NotImplementedError("TargetPath.link_to() is unsupported")
432
+
433
+ def exists(self) -> bool:
434
+ """
435
+ Whether this path exists.
436
+ """
437
+ try:
438
+ # .exists() must resolve possible symlinks
439
+ self.get().stat()
440
+ return True
441
+ except (FileNotFoundError, NotADirectoryError, NotASymlinkError, SymlinkRecursionError, ValueError):
442
+ return False
443
+
444
+ def is_dir(self) -> bool:
445
+ """
446
+ Whether this path is a directory.
447
+ """
448
+ try:
449
+ return self.get().is_dir()
450
+ except (FilesystemError, ValueError):
451
+ return False
452
+
453
+ def is_file(self) -> bool:
454
+ """
455
+ Whether this path is a regular file (also True for symlinks pointing
456
+ to regular files).
457
+ """
458
+ try:
459
+ return self.get().is_file()
460
+ except (FilesystemError, ValueError):
461
+ return False
462
+
463
+ def is_mount(self) -> bool:
464
+ """
465
+ Check if this path is a POSIX mount point
466
+ """
467
+ # Need to exist and be a dir
468
+ if not self.exists() or not self.is_dir():
469
+ return False
470
+
471
+ try:
472
+ parent_dev = self.parent.stat().st_dev
473
+ except FilesystemError:
474
+ return False
475
+
476
+ dev = self.stat().st_dev
477
+ if dev != parent_dev:
478
+ return True
479
+ ino = self.stat().st_ino
480
+ parent_ino = self.parent.stat().st_ino
481
+ return ino == parent_ino
482
+
483
+ def is_symlink(self) -> bool:
484
+ """
485
+ Whether this path is a symbolic link.
486
+ """
487
+ try:
488
+ return self.get().is_symlink()
489
+ except (FilesystemError, ValueError):
490
+ return False
491
+
492
+ # NOTE: Forward compatibility with CPython >= 3.12
493
+ def is_junction(self) -> bool:
494
+ """
495
+ Whether this path is a junction.
496
+ """
497
+ return isjunction(self)
498
+
499
+ def is_block_device(self) -> bool:
500
+ """
501
+ Whether this path is a block device.
502
+ """
503
+ try:
504
+ return S_ISBLK(self.stat().st_mode)
505
+ except (FilesystemError, ValueError):
506
+ return False
507
+
508
+ def is_char_device(self) -> bool:
509
+ """
510
+ Whether this path is a character device.
511
+ """
512
+ try:
513
+ return S_ISCHR(self.stat().st_mode)
514
+ except (FilesystemError, ValueError):
515
+ return False
516
+
517
+ def is_fifo(self) -> bool:
518
+ """
519
+ Whether this path is a FIFO.
520
+ """
521
+ try:
522
+ return S_ISFIFO(self.stat().st_mode)
523
+ except (FilesystemError, ValueError):
524
+ return False
525
+
526
+ def is_socket(self) -> bool:
527
+ """
528
+ Whether this path is a socket.
529
+ """
530
+ try:
531
+ return S_ISSOCK(self.stat().st_mode)
532
+ except (FilesystemError, ValueError):
533
+ return False
534
+
535
+ def expanduser(self) -> TargetPath:
536
+ """Return a new path with expanded ~ and ~user constructs
537
+ (as returned by os.path.expanduser)
538
+ """
539
+ raise NotImplementedError("TargetPath.expanduser() is unsupported")