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