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
@@ -1,20 +1,16 @@
1
- """Pathlib like abstraction helpers for target filesystem.
2
-
3
- Also contains some other filesystem related utilities.
4
- """
1
+ """Filesystem and path related utilities."""
5
2
 
6
3
  from __future__ import annotations
7
4
 
8
- import errno
9
5
  import fnmatch
10
6
  import gzip
11
7
  import hashlib
12
8
  import io
13
9
  import logging
14
10
  import os
15
- import posixpath
16
11
  import re
17
- from pathlib import Path, PurePath, _PathParents, _PosixFlavour
12
+ import sys
13
+ from pathlib import Path
18
14
  from typing import Any, BinaryIO, Iterator, Optional, Sequence, TextIO, Union
19
15
 
20
16
  try:
@@ -25,66 +21,67 @@ except ImportError:
25
21
  HAVE_BZ2 = False
26
22
 
27
23
  import dissect.target.filesystem as filesystem
28
- from dissect.target.exceptions import (
29
- FileNotFoundError,
30
- NotADirectoryError,
31
- NotASymlinkError,
32
- SymlinkRecursionError,
24
+ from dissect.target.exceptions import FileNotFoundError, SymlinkRecursionError
25
+ from dissect.target.helpers.polypath import (
26
+ abspath,
27
+ basename,
28
+ commonpath,
29
+ dirname,
30
+ isabs,
31
+ join,
32
+ normalize,
33
+ normpath,
34
+ relpath,
35
+ split,
36
+ splitdrive,
37
+ splitext,
38
+ splitroot,
33
39
  )
34
40
 
35
- log = logging.getLogger(__name__)
36
-
37
- re_normalize_path = re.compile(r"[/]+")
38
- re_normalize_sbs_path = re.compile(r"[\\/]+")
39
- re_glob_magic = re.compile(r"[*?[]")
40
- re_glob_index = re.compile(r"(?<=\/)[^\/]*[*?[]\/?")
41
-
42
-
43
- def normalize(path: str, alt_separator: str = "") -> str:
44
- if alt_separator == "\\":
45
- return re_normalize_sbs_path.sub("/", path)
46
- else:
47
- return re_normalize_path.sub("/", path)
48
-
49
-
50
- def join(*args, alt_separator: str = "") -> str:
51
- return posixpath.join(*[normalize(part, alt_separator=alt_separator) for part in args])
52
-
53
-
54
- def dirname(path: str, alt_separator: str = "") -> str:
55
- return posixpath.dirname(normalize(path, alt_separator=alt_separator))
56
-
57
-
58
- def basename(path: str, alt_separator: str = "") -> str:
59
- return posixpath.basename(normalize(path, alt_separator=alt_separator))
41
+ if sys.version_info >= (3, 12):
42
+ from dissect.target.helpers.compat.path_312 import PureDissectPath, TargetPath
43
+ elif sys.version_info >= (3, 11):
44
+ from dissect.target.helpers.compat.path_311 import PureDissectPath, TargetPath
45
+ elif sys.version_info >= (3, 10):
46
+ from dissect.target.helpers.compat.path_310 import PureDissectPath, TargetPath
47
+ elif sys.version_info >= (3, 9):
48
+ from dissect.target.helpers.compat.path_39 import PureDissectPath, TargetPath
49
+ else:
50
+ raise RuntimeError("dissect.target requires at least Python 3.9")
60
51
 
61
52
 
62
- def split(path: str, alt_separator: str = "") -> str:
63
- return posixpath.split(normalize(path, alt_separator=alt_separator))
64
-
65
-
66
- def isabs(path: str, alt_separator: str = "") -> str:
67
- return posixpath.isabs(normalize(path, alt_separator=alt_separator))
68
-
69
-
70
- def normpath(path: str, alt_separator: str = "") -> str:
71
- return posixpath.normpath(normalize(path, alt_separator=alt_separator))
72
-
73
-
74
- def abspath(path: str, cwd: str = "", alt_separator: str = "") -> str:
75
- cwd = cwd or "/"
76
- cwd = normalize(cwd, alt_separator=alt_separator)
77
- path = normalize(path, alt_separator=alt_separator)
78
- if not isabs(path):
79
- path = join(cwd, path)
80
- return posixpath.normpath(path)
81
-
53
+ log = logging.getLogger(__name__)
82
54
 
83
- def relpath(path: str, start: str, alt_separator: str = "") -> str:
84
- return posixpath.relpath(
85
- normalize(path, alt_separator=alt_separator),
86
- normalize(start, alt_separator=alt_separator),
87
- )
55
+ re_glob_magic = re.compile(r"[*?[]")
56
+ re_glob_index = re.compile(r"(?<=\/)[^\/]*[*?[]")
57
+
58
+ __all__ = [
59
+ "abspath",
60
+ "basename",
61
+ "commonpath",
62
+ "dirname",
63
+ "fs_attrs",
64
+ "generate_addr",
65
+ "glob_ext",
66
+ "glob_split",
67
+ "isabs",
68
+ "join",
69
+ "normalize",
70
+ "normpath",
71
+ "open_decompress",
72
+ "PureDissectPath",
73
+ "relpath",
74
+ "resolve_link",
75
+ "reverse_readlines",
76
+ "split",
77
+ "splitdrive",
78
+ "splitext",
79
+ "splitroot",
80
+ "stat_result",
81
+ "TargetPath",
82
+ "walk_ext",
83
+ "walk",
84
+ ]
88
85
 
89
86
 
90
87
  def generate_addr(path: Union[str, Path], alt_separator: str = "") -> int:
@@ -94,9 +91,6 @@ def generate_addr(path: Union[str, Path], alt_separator: str = "") -> int:
94
91
  return int(hashlib.sha256(path.encode()).hexdigest()[:8], 16)
95
92
 
96
93
 
97
- splitext = posixpath.splitext
98
-
99
-
100
94
  class stat_result: # noqa
101
95
  """Custom stat_result object, designed to mimick os.stat_result.
102
96
 
@@ -240,583 +234,6 @@ class stat_result: # noqa
240
234
  return st
241
235
 
242
236
 
243
- # fmt: off
244
- """
245
- A pathlib.Path compatible implementation for dissect.target starts here. This allows for the
246
- majority of the pathlib.Path API to "just work" on dissect.target filesystems.
247
-
248
- Most of this consists of subclassed internal classes with dissect.target specific patches,
249
- but sometimes the change to a function is small, so the entire internal function is copied
250
- and only a small part changed. To ease updating this code, the order of functions, comments
251
- and code style is kept exactly the same as the original pathlib.py.
252
-
253
- Yes, we know, this is playing with fire and it can break on new CPython releases.
254
-
255
- Commit hash of CPython we're currently in sync with: 9f101c23a41e739f5f80cf38419df1281835d452
256
-
257
- Notes:
258
- - CPython 3.11 ditched the _Accessor class, so we override the methods that should use it
259
- """
260
-
261
-
262
- class _DissectFlavour(_PosixFlavour):
263
- is_supported = True
264
-
265
- __variant_instances = {}
266
-
267
- def __new__(cls, case_sensitive=False, alt_separator=None):
268
- idx = (case_sensitive, alt_separator)
269
- instance = cls.__variant_instances.get(idx, None)
270
- if instance is None:
271
- instance = _PosixFlavour.__new__(cls)
272
- cls.__variant_instances[idx] = instance
273
-
274
- return instance
275
-
276
- def __init__(self, case_sensitive=False, alt_separator=""):
277
- super().__init__()
278
- self.altsep = alt_separator
279
- self.case_sensitive = case_sensitive
280
-
281
- def casefold(self, s):
282
- return s if self.case_sensitive else s.lower()
283
-
284
- def casefold_parts(self, parts):
285
- return parts if self.case_sensitive else [p.lower() for p in parts]
286
-
287
- def compile_pattern(self, pattern):
288
- return re.compile(fnmatch.translate(pattern), 0 if self.case_sensitive else re.IGNORECASE).fullmatch
289
-
290
- # CPython <= 3.9
291
- def resolve(self, path, strict=False):
292
- sep = self.sep
293
- accessor = path._accessor
294
- seen = {}
295
-
296
- def _resolve(fs, path, rest):
297
- if rest.startswith(sep):
298
- path = ''
299
-
300
- for name in rest.split(sep):
301
- if not name or name == '.':
302
- # current dir
303
- continue
304
- if name == '..':
305
- # parent dir
306
- path, _, _ = path.rpartition(sep)
307
- continue
308
- if path.endswith(sep):
309
- newpath = path + name
310
- else:
311
- newpath = path + sep + name
312
- if newpath in seen:
313
- # Already seen this path
314
- path = seen[newpath]
315
- if path is not None:
316
- # use cached value
317
- continue
318
- # The symlink is not resolved, so we must have a symlink loop.
319
- raise RuntimeError("Symlink loop from %r" % newpath)
320
- # Resolve the symbolic link
321
- try:
322
- target = accessor.readlink(fs.path(newpath))
323
- except OSError as e:
324
- if e.errno != errno.EINVAL and strict:
325
- raise
326
- # Not a symlink, or non-strict mode. We just leave the path
327
- # untouched.
328
- path = newpath
329
- else:
330
- seen[newpath] = None # not resolved symlink
331
- path = _resolve(fs, path, target)
332
- seen[newpath] = path # resolved symlink
333
-
334
- return path
335
-
336
- return _resolve(path._fs, '', str(path)) or sep
337
-
338
- # CPython <= 3.9
339
- def gethomedir(self, username):
340
- raise NotImplementedError()
341
-
342
-
343
- def _get_oserror(path):
344
- # We emulate some OSError exceptions to play nice with pathlib
345
- try:
346
- return path.get()
347
- except FileNotFoundError:
348
- e = OSError(errno.ENOENT)
349
- e.errno = errno.ENOENT
350
- raise e
351
- except NotADirectoryError:
352
- e = OSError(errno.ENOTDIR)
353
- e.errno = errno.ENOTDIR
354
- raise e
355
-
356
-
357
- class _DissectScandirIterator:
358
- """This class implements a ScandirIterator for dissect's scandir()
359
-
360
- The _DissectScandirIterator provides a context manager, so scandir can be called as:
361
-
362
- ```
363
- with scandir(path) as it:
364
- for entry in it
365
- print(entry.name)
366
- ```
367
-
368
- similar to os.scandir() behaviour since Python 3.6.
369
- """
370
-
371
- def __init__(self, iterator):
372
- self._iterator = iterator
373
-
374
- def __del__(self):
375
- self.close()
376
-
377
- def __enter__(self):
378
- return self._iterator
379
-
380
- def __exit__(self, *args, **kwargs):
381
- return False
382
-
383
- def __iter__(self):
384
- return self._iterator
385
-
386
- def __next__(self, *args):
387
- return next(self._iterator, *args)
388
-
389
- def close(self):
390
- # close() is not defined in the various filesystem implementations. The
391
- # python ScandirIterator does define the interface however.
392
- pass
393
-
394
-
395
- class _DissectAccessor:
396
- # CPython >= 3.10
397
- @staticmethod
398
- def stat(path, follow_symlinks=True):
399
- if follow_symlinks:
400
- return path.get().stat()
401
- else:
402
- return path.get().lstat()
403
-
404
- # CPython <= 3.9
405
- @staticmethod
406
- def lstat(path):
407
- return path.get().lstat()
408
-
409
- @staticmethod
410
- def open(path, mode='rb', buffering=0, encoding=None,
411
- errors=None, newline=None, *args, **kwargs):
412
- """Open file and return a stream.
413
-
414
- Supports a subset of features of the real pathlib.open/io.open.
415
-
416
- Note: in contrast to regular Python, the mode is binary by default. Text mode
417
- has to be explicitly specified. Buffering is also disabled by default.
418
- """
419
- modes = set(mode)
420
- if modes - set('rbt') or len(mode) > len(modes):
421
- raise ValueError("invalid mode: %r" % mode)
422
-
423
- reading = 'r' in modes
424
- binary = 'b' in modes
425
- text = 't' in modes or 'b' not in modes
426
-
427
- if not reading:
428
- raise ValueError("must be reading mode")
429
- if text and binary:
430
- raise ValueError("can't have text and binary mode at once")
431
- if binary and encoding is not None:
432
- raise ValueError("binary mode doesn't take an encoding argument")
433
- if binary and errors is not None:
434
- raise ValueError("binary mode doesn't take an errors argument")
435
- if binary and newline is not None:
436
- raise ValueError("binary mode doesn't take a newline argument")
437
-
438
- raw = path.get().open()
439
- result = raw
440
-
441
- line_buffering = False
442
- if buffering == 1 or buffering < 0 and raw.isatty():
443
- buffering = -1
444
- line_buffering = True
445
- if buffering < 0 or text and buffering == 0:
446
- buffering = io.DEFAULT_BUFFER_SIZE
447
- if buffering == 0:
448
- if binary:
449
- return result
450
- raise ValueError("can't have unbuffered text I/O")
451
-
452
- buffer = io.BufferedReader(raw, buffering)
453
- result = buffer
454
- if binary:
455
- return result
456
-
457
- result = io.TextIOWrapper(buffer, encoding, errors, newline, line_buffering)
458
- result.mode = mode
459
-
460
- return result
461
-
462
- @staticmethod
463
- def listdir(path):
464
- return path.get().listdir()
465
-
466
- @staticmethod
467
- def scandir(path):
468
- return _DissectScandirIterator(path.get().scandir())
469
-
470
- @staticmethod
471
- def chmod(*args, **kwargs):
472
- raise NotImplementedError()
473
-
474
- @staticmethod
475
- def lchmod(*args, **kwargs):
476
- raise NotImplementedError()
477
-
478
- @staticmethod
479
- def mkdir(*args, **kwargs):
480
- raise NotImplementedError()
481
-
482
- @staticmethod
483
- def unlink(*args, **kwargs):
484
- raise NotImplementedError()
485
-
486
- # CPython >= 3.10
487
- @staticmethod
488
- def link(*args, **kwargs):
489
- raise NotImplementedError()
490
-
491
- # CPython <= 3.9
492
- @staticmethod
493
- def link_to(*args, **kwargs):
494
- raise NotImplementedError()
495
-
496
- @staticmethod
497
- def rmdir(*args, **kwargs):
498
- raise NotImplementedError()
499
-
500
- @staticmethod
501
- def rename(*args, **kwargs):
502
- raise NotImplementedError()
503
-
504
- @staticmethod
505
- def replace(*args, **kwargs):
506
- raise NotImplementedError()
507
-
508
- @staticmethod
509
- def symlink(*args, **kwargs):
510
- raise NotImplementedError()
511
-
512
- # CPython >= 3.10
513
- @staticmethod
514
- def touch(*args, **kwargs):
515
- raise NotImplementedError()
516
-
517
- # CPython <= 3.9
518
- @staticmethod
519
- def utime(*args, **kwargs):
520
- raise NotImplementedError()
521
-
522
- @staticmethod
523
- def readlink(path):
524
- entry = _get_oserror(path)
525
- if not entry.is_symlink():
526
- e = OSError(errno.EINVAL)
527
- e.errno = errno.EINVAL
528
- raise e
529
- return entry.readlink()
530
-
531
- @staticmethod
532
- def owner(*args, **kwargs):
533
- raise NotImplementedError()
534
-
535
- @staticmethod
536
- def group(*args, **kwargs):
537
- raise NotImplementedError()
538
-
539
- # CPython >= 3.10
540
- @staticmethod
541
- def getcwd(*args, **kwargs):
542
- raise NotImplementedError()
543
-
544
- # CPython >= 3.10
545
- @staticmethod
546
- def expanduser(*args, **kwargs):
547
- raise NotImplementedError()
548
-
549
- # CPython >= 3.10
550
- @staticmethod
551
- def realpath(*args, **kwargs):
552
- raise NotImplementedError()
553
-
554
-
555
- _dissect_accessor = _DissectAccessor()
556
-
557
-
558
- class _DissectPathParents(_PathParents):
559
- __slots__ = ('_fs')
560
-
561
- def __init__(self, path):
562
- super().__init__(path)
563
- self._fs = path._fs
564
- self._flavour = path._flavour
565
-
566
- def __getitem__(self, idx):
567
- result = super().__getitem__(idx)
568
- result._fs = self._fs
569
- result._flavour = self._flavour
570
- return result
571
-
572
-
573
- class PureDissectPath(PurePath):
574
- _flavour = _DissectFlavour(case_sensitive=False)
575
-
576
- def __reduce__(self):
577
- raise TypeError("pickling is currently not supported")
578
-
579
- @classmethod
580
- def _from_parts(cls, args, *_args, **_kwargs):
581
- fs = args[0]
582
-
583
- if not isinstance(fs, filesystem.Filesystem):
584
- raise TypeError(
585
- "invalid PureDissectPath initialization: missing filesystem, "
586
- "got %r (this might be a bug, please report)"
587
- % args
588
- )
589
-
590
- alt_separator = fs.alt_separator
591
- path_args = []
592
- for arg in args[1:]:
593
- if isinstance(arg, str):
594
- arg = normalize(arg, alt_separator=alt_separator)
595
- path_args.append(arg)
596
-
597
- self = super()._from_parts(path_args, *_args, **_kwargs)
598
- self._fs = fs
599
-
600
- self._flavour = _DissectFlavour(
601
- alt_separator=fs.alt_separator,
602
- case_sensitive=fs.case_sensitive
603
- )
604
-
605
- return self
606
-
607
- def _make_child(self, args):
608
- child = super()._make_child(args)
609
- child._fs = self._fs
610
- child._flavour = self._flavour
611
- return child
612
-
613
- def with_name(self, name):
614
- result = super().with_name(name)
615
- result._fs = self._fs
616
- result._flavour = self._flavour
617
- return result
618
-
619
- def with_stem(self, stem):
620
- result = super().with_stem(stem)
621
- result._fs = self._fs
622
- result._flavour = self._flavour
623
- return result
624
-
625
- def with_suffix(self, suffix):
626
- result = super().with_suffix(suffix)
627
- result._fs = self._fs
628
- result._flavour = self._flavour
629
- return result
630
-
631
- def relative_to(self, *other):
632
- result = super().relative_to(*other)
633
- result._fs = self._fs
634
- result._flavour = self._flavour
635
- return result
636
-
637
- def __rtruediv__(self, key):
638
- try:
639
- return self._from_parts([self._fs, key] + self._parts)
640
- except TypeError:
641
- return NotImplemented
642
-
643
- @property
644
- def parent(self):
645
- result = super().parent
646
- result._fs = self._fs
647
- result._flavour = self._flavour
648
- return result
649
-
650
- @property
651
- def parents(self):
652
- return _DissectPathParents(self)
653
-
654
-
655
- class TargetPath(Path, PureDissectPath):
656
- # CPython >= 3.10
657
- _accessor = _dissect_accessor
658
- __slots__ = '_entry'
659
-
660
- # CPython <= 3.9
661
- def _init(self, template=None):
662
- self._accessor = _dissect_accessor
663
-
664
- def _make_child_relpath(self, part):
665
- child = super()._make_child_relpath(part)
666
- child._fs = self._fs
667
- child._flavour = self._flavour
668
- return child
669
-
670
- def get(self):
671
- try:
672
- return self._entry
673
- except AttributeError:
674
- self._entry = self._fs.get(str(self))
675
- return self._entry
676
-
677
- @classmethod
678
- def cwd(cls):
679
- raise NotImplementedError()
680
-
681
- @classmethod
682
- def home(cls):
683
- raise NotImplementedError()
684
-
685
- def iterdir(self):
686
- for entry in self._accessor.scandir(self):
687
- if entry.name in {'.', '..'}:
688
- # Yielding a path object for these makes little sense
689
- continue
690
- child_path = self._make_child_relpath(entry.name)
691
- child_path._entry = entry
692
- yield child_path
693
-
694
- def _scandir(self):
695
- return self._accessor.scandir(self)
696
-
697
- def absolute(self):
698
- raise NotImplementedError()
699
-
700
- def resolve(self, strict=False):
701
- s = self._flavour.resolve(self)
702
- if s is None:
703
- # No symlink resolution => for consistency, raise an error if
704
- # the path doesn't exist or is forbidden
705
- self.stat()
706
- s = str(self.absolute())
707
- # Now we have no symlinks in the path, it's safe to normalize it.
708
- normed = self._flavour.pathmod.normpath(s)
709
- obj = self._from_parts((self._fs, normed,))
710
- return obj
711
-
712
- # CPython >= 3.11
713
- def stat(self, *, follow_symlinks=True):
714
- """
715
- Return the result of the stat() system call on this path, like
716
- os.stat() does.
717
- """
718
- return self._accessor.stat(self, follow_symlinks=follow_symlinks)
719
-
720
- def owner(self):
721
- raise NotImplementedError()
722
-
723
- def group(self):
724
- raise NotImplementedError()
725
-
726
- def open(self, mode='rb', buffering=0, encoding=None,
727
- errors=None, newline=None):
728
-
729
- if "b" not in mode:
730
- encoding = encoding or "UTF-8"
731
- # CPython >= 3.10
732
- if hasattr(io, "text_encoding"):
733
- # Vermin linting needs to be skipped for this line as this is
734
- # guarded by an explicit check for availability.
735
- # novermin
736
- encoding = io.text_encoding(encoding)
737
- return self._accessor.open(self, mode, buffering, encoding, errors,
738
- newline)
739
-
740
- def write_bytes(self, *args, **kwargs):
741
- raise NotImplementedError()
742
-
743
- def write_text(self, *args, **kwargs):
744
- raise NotImplementedError()
745
-
746
- def readlink(self):
747
- """
748
- Return the path to which the symbolic link points.
749
- """
750
- path = self._accessor.readlink(self)
751
- obj = self._from_parts((self._fs, path,))
752
- return obj
753
-
754
- def touch(self, *args, **kwargs):
755
- raise NotImplementedError()
756
-
757
- def mkdir(self, *args, **kwargs):
758
- raise NotImplementedError()
759
-
760
- def chmod(self, *args, **kwargs):
761
- raise NotImplementedError()
762
-
763
- def lchmod(self, *args, **kwargs):
764
- raise NotImplementedError()
765
-
766
- def unlink(self):
767
- raise NotImplementedError()
768
-
769
- def rmdir(self):
770
- raise NotImplementedError()
771
-
772
- def rename(self, *args, **kwargs):
773
- raise NotImplementedError()
774
-
775
- def replace(self, *args, **kwargs):
776
- raise NotImplementedError()
777
-
778
- def symlink_to(self, *args, **kwargs):
779
- raise NotImplementedError()
780
-
781
- def hardlink_to(self, target):
782
- raise NotImplementedError()
783
-
784
- def link_to(self, *args, **kwargs):
785
- raise NotImplementedError()
786
-
787
- def exists(self):
788
- try:
789
- # .exists() must resolve possible symlinks
790
- self.get().stat()
791
- return True
792
- except (FileNotFoundError, NotADirectoryError, NotASymlinkError, SymlinkRecursionError, ValueError):
793
- return False
794
-
795
- def is_dir(self):
796
- try:
797
- return self.get().is_dir()
798
- except (FileNotFoundError, NotADirectoryError, NotASymlinkError, SymlinkRecursionError, ValueError):
799
- return False
800
-
801
- def is_file(self):
802
- try:
803
- return self.get().is_file()
804
- except (FileNotFoundError, NotADirectoryError, NotASymlinkError, SymlinkRecursionError, ValueError):
805
- return False
806
-
807
- def is_symlink(self):
808
- try:
809
- return self.get().is_symlink()
810
- except (FileNotFoundError, NotADirectoryError, NotASymlinkError, SymlinkRecursionError, ValueError):
811
- return False
812
-
813
- def expanduser(self):
814
- raise NotImplementedError()
815
-
816
-
817
- # fmt: on
818
-
819
-
820
237
  def walk(path_entry, topdown=True, onerror=None, followlinks=False):
821
238
  for path_list, dirs, files in walk_ext(path_entry, topdown, onerror, followlinks):
822
239
  dir_names = [d.name for d in dirs]
@@ -857,7 +274,17 @@ def walk_ext(path_entry, topdown=True, onerror=None, followlinks=False):
857
274
  yield [path_entry], dirs, files
858
275
 
859
276
 
860
- def glob_split(pattern: str, alt_separator: str = "") -> str:
277
+ def glob_split(pattern: str, alt_separator: str = "") -> tuple[str, str]:
278
+ """Split a pattern on path part boundaries on the first path part with a glob pattern.
279
+
280
+ Args:
281
+ pattern: A glob pattern to match names of filesystem entries against.
282
+ alt_separator: An optional alternative path separator in use by the filesystem being matched.
283
+
284
+ Returns:
285
+ A tuple of a string with path parts up to the first path part that has a glob pattern and a string of
286
+ the remaining path parts.
287
+ """
861
288
  # re_glob_index expects a normalized pattern
862
289
  pattern = normalize(pattern, alt_separator=alt_separator)
863
290
 
@@ -870,75 +297,110 @@ def glob_split(pattern: str, alt_separator: str = "") -> str:
870
297
  return pattern[:pos], pattern[pos:]
871
298
 
872
299
 
873
- def glob_ext(direntry: filesystem.FilesystemEntry, pattern: str) -> filesystem.FilesystemEntry:
300
+ def glob_ext(direntry: filesystem.FilesystemEntry, pattern: str) -> Iterator[filesystem.FilesystemEntry]:
301
+ """Recursively search and return filesystem entries matching a given glob pattern.
302
+
303
+ Args:
304
+ direntry: The filesystem entry relative to which to search.
305
+ pattern: A glob pattern to match names of filesystem entries against.
306
+
307
+ Yields:
308
+ Matching filesystem entries (files and/or directories).
309
+ """
310
+
311
+ # Split the pattern on the last path part. base_name will contain the last path part (which is
312
+ # '' if pattern ends with a /) and dir_name will contain the other parts.
874
313
  dir_name, base_name = split(pattern, alt_separator=direntry.fs.alt_separator)
875
314
 
315
+ # The simple case where there are no globs.
876
316
  if not has_glob_magic(pattern):
877
317
  try:
878
318
  entry = direntry.get(pattern)
879
319
  except FileNotFoundError:
880
320
  pass
881
321
  else:
322
+ # Patterns ending with a slash, so without a base_name, should match only directories.
882
323
  if base_name:
883
324
  yield entry
884
- # Patterns ending with a slash should match only directories
885
325
  elif entry.is_dir():
886
326
  yield entry
887
327
  return
888
328
 
329
+ # The pattern has only one path part, so we can match directly against the files in direntry.
889
330
  if not dir_name:
890
331
  for entry in glob_ext1(direntry, base_name):
891
332
  yield entry
892
333
  return
893
334
 
894
- if dir_name != pattern and has_glob_magic(dir_name):
895
- dirs = glob_ext(direntry, dir_name)
335
+ # If the pattern has more than one path part and these parts (dir_name) contain globs, we
336
+ # recursively go over all the path parts and match them (glob_ext).
337
+ # If these path parts have no globs, we get the path directly by name (glob_ext0).
338
+ if has_glob_magic(dir_name):
339
+ glob_in_dir = glob_ext
896
340
  else:
897
- dirs = [dir_name]
341
+ glob_in_dir = glob_ext0
898
342
 
343
+ # If the pattern's last path part (base_name) has globs, we fnmatch it against the entries in
344
+ # direntry (glob_ext1), otherwise we get the part directly by name (glob_ext0).
899
345
  if has_glob_magic(base_name):
900
- glob_in_dir = glob_ext1
346
+ glob_in_base = glob_ext1
901
347
  else:
902
- glob_in_dir = glob_ext0
348
+ glob_in_base = glob_ext0
903
349
 
904
- for direntry in dirs:
905
- for entry in glob_in_dir(direntry, base_name):
350
+ for direntry in glob_in_dir(direntry, dir_name):
351
+ for entry in glob_in_base(direntry, base_name):
906
352
  yield entry
907
353
 
908
354
 
909
355
  # These 2 helper functions non-recursively glob inside a literal directory.
910
- # They return a list of basenames. `glob1` accepts a pattern while `glob0`
911
- # takes a literal base_name (so it only has to check for its existence).
356
+ def glob_ext1(direntry: filesystem.FilesystemEntry, pattern: str) -> Iterator[filesystem.FilesystemEntry]:
357
+ """Match and return filesystem entries in a given filesystem entry based on pattern.
912
358
 
359
+ Args:
360
+ direntry: The filesystem entry relative to which to match the entries.
361
+ pattern: A glob pattern to match names of filesystem entries against.
913
362
 
914
- def glob_ext1(direntry: filesystem.FilesystemEntry, pattern: str) -> filesystem.FilesystemEntry:
363
+ Yields:
364
+ Matching filesystem entries (files and/or directories).
365
+ """
915
366
  if not direntry.is_dir():
916
367
  return
917
368
 
918
369
  entries = direntry.scandir()
919
370
 
920
371
  if pattern[0] != ".":
372
+ # Do not return dot-files, unless they are explicitly searched for.
921
373
  entries = filter(lambda x: x.name[0] != ".", entries)
922
374
 
923
- for e in entries:
924
- name = e.name if e.fs.case_sensitive else e.name.lower()
925
- pattern = pattern if e.fs.case_sensitive else pattern.lower()
375
+ for entry in entries:
376
+ case_sensitive = entry.fs.case_sensitive
377
+ name = entry.name if case_sensitive else entry.name.lower()
378
+ pattern = pattern if case_sensitive else pattern.lower()
926
379
  if fnmatch.fnmatch(name, pattern):
927
- yield e
380
+ yield entry
381
+
928
382
 
383
+ def glob_ext0(direntry: filesystem.FilesystemEntry, path: str) -> Iterator[filesystem.FilesystemEntry]:
384
+ """Return the filesystem entry equal to the given path relative to direntry.
929
385
 
930
- def glob_ext0(direntry: filesystem.FilesystemEntry, base_name: str) -> list[filesystem.FilesystemEntry]:
931
- if base_name == "":
932
- # `os.path.split()` returns an empty base_name for paths ending with a
933
- # directory separator. 'q*x/' should match only directories.
386
+ Args:
387
+ direntry: The filesystem entry relative to which to return the path.
388
+ path: The path to the filesystem entry to return (if present). If path
389
+ is an empty string (``''``) the ``direntry`` itself is returned.
390
+
391
+ Yields:
392
+ The matching filesystem entry (file or directory).
393
+ """
394
+ if path == "":
395
+ # os.path.split() returns an empty path for paths ending with a directory separator.
396
+ # E.g. 'q*x/' should match only directories.
934
397
  if direntry.is_dir():
935
- return [direntry]
398
+ yield direntry
936
399
  elif direntry.is_dir():
937
400
  try:
938
- return [direntry.get(base_name)]
401
+ yield direntry.get(path)
939
402
  except FileNotFoundError:
940
403
  pass
941
- return []
942
404
 
943
405
 
944
406
  def has_glob_magic(s) -> bool: