dissect.target 3.14.dev28__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.
- dissect/target/containers/ewf.py +1 -1
- dissect/target/containers/vhd.py +5 -2
- dissect/target/filesystem.py +36 -18
- dissect/target/filesystems/dir.py +10 -4
- dissect/target/filesystems/jffs.py +122 -0
- dissect/target/helpers/compat/path_310.py +506 -0
- dissect/target/helpers/compat/path_311.py +539 -0
- dissect/target/helpers/compat/path_312.py +443 -0
- dissect/target/helpers/compat/path_39.py +545 -0
- dissect/target/helpers/compat/path_common.py +223 -0
- dissect/target/helpers/cyber.py +512 -0
- dissect/target/helpers/fsutil.py +128 -666
- dissect/target/helpers/hashutil.py +17 -57
- dissect/target/helpers/keychain.py +9 -3
- dissect/target/helpers/loaderutil.py +1 -1
- dissect/target/helpers/mount.py +47 -4
- dissect/target/helpers/polypath.py +73 -0
- dissect/target/helpers/record_modifier.py +100 -0
- dissect/target/loader.py +2 -1
- dissect/target/loaders/asdf.py +2 -0
- dissect/target/loaders/cyber.py +37 -0
- dissect/target/loaders/log.py +14 -3
- dissect/target/loaders/raw.py +2 -0
- dissect/target/loaders/remote.py +12 -0
- dissect/target/loaders/tar.py +13 -0
- dissect/target/loaders/targetd.py +2 -0
- dissect/target/loaders/velociraptor.py +12 -3
- dissect/target/loaders/vmwarevm.py +2 -0
- dissect/target/plugin.py +272 -143
- dissect/target/plugins/apps/ssh/openssh.py +11 -54
- dissect/target/plugins/apps/ssh/opensshd.py +4 -3
- dissect/target/plugins/apps/ssh/putty.py +236 -0
- dissect/target/plugins/apps/ssh/ssh.py +58 -0
- dissect/target/plugins/apps/vpn/openvpn.py +6 -0
- dissect/target/plugins/apps/webserver/apache.py +309 -95
- dissect/target/plugins/apps/webserver/caddy.py +5 -2
- dissect/target/plugins/apps/webserver/citrix.py +82 -0
- dissect/target/plugins/apps/webserver/iis.py +9 -12
- dissect/target/plugins/apps/webserver/nginx.py +5 -2
- dissect/target/plugins/apps/webserver/webserver.py +25 -41
- dissect/target/plugins/child/wsl.py +1 -1
- dissect/target/plugins/filesystem/ntfs/mft.py +10 -0
- dissect/target/plugins/filesystem/ntfs/mft_timeline.py +10 -0
- dissect/target/plugins/filesystem/ntfs/usnjrnl.py +10 -0
- dissect/target/plugins/filesystem/ntfs/utils.py +28 -5
- dissect/target/plugins/filesystem/resolver.py +6 -4
- dissect/target/plugins/general/default.py +0 -2
- dissect/target/plugins/general/example.py +0 -1
- dissect/target/plugins/general/loaders.py +3 -5
- dissect/target/plugins/os/unix/_os.py +3 -3
- dissect/target/plugins/os/unix/bsd/citrix/_os.py +68 -28
- dissect/target/plugins/os/unix/bsd/citrix/history.py +130 -0
- dissect/target/plugins/os/unix/generic.py +17 -10
- dissect/target/plugins/os/unix/linux/fortios/__init__.py +0 -0
- dissect/target/plugins/os/unix/linux/fortios/_os.py +534 -0
- dissect/target/plugins/os/unix/linux/fortios/generic.py +30 -0
- dissect/target/plugins/os/unix/linux/fortios/locale.py +109 -0
- dissect/target/plugins/os/windows/log/evt.py +1 -1
- dissect/target/plugins/os/windows/log/schedlgu.py +155 -0
- dissect/target/plugins/os/windows/regf/firewall.py +1 -1
- dissect/target/plugins/os/windows/regf/shimcache.py +1 -1
- dissect/target/plugins/os/windows/regf/trusteddocs.py +1 -1
- dissect/target/plugins/os/windows/registry.py +1 -1
- dissect/target/plugins/os/windows/sam.py +3 -0
- dissect/target/plugins/os/windows/sru.py +41 -28
- dissect/target/plugins/os/windows/tasks.py +5 -2
- dissect/target/target.py +7 -3
- dissect/target/tools/dd.py +7 -1
- dissect/target/tools/fs.py +8 -1
- dissect/target/tools/info.py +22 -15
- dissect/target/tools/mount.py +28 -3
- dissect/target/tools/query.py +146 -117
- dissect/target/tools/reg.py +21 -16
- dissect/target/tools/shell.py +30 -6
- dissect/target/tools/utils.py +28 -0
- dissect/target/volumes/bde.py +14 -10
- dissect/target/volumes/luks.py +18 -10
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/METADATA +4 -3
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/RECORD +85 -67
- dissect/target/plugins/os/unix/linux/fortigate/_os.py +0 -175
- /dissect/target/{plugins/os/unix/linux/fortigate → helpers/compat}/__init__.py +0 -0
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/LICENSE +0 -0
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/WHEEL +0 -0
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/top_level.txt +0 -0
dissect/target/helpers/fsutil.py
CHANGED
@@ -1,20 +1,16 @@
|
|
1
|
-
"""
|
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
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
895
|
-
|
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
|
-
|
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
|
-
|
346
|
+
glob_in_base = glob_ext1
|
901
347
|
else:
|
902
|
-
|
348
|
+
glob_in_base = glob_ext0
|
903
349
|
|
904
|
-
for direntry in
|
905
|
-
for entry in
|
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
|
-
|
911
|
-
|
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
|
-
|
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
|
924
|
-
|
925
|
-
|
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
|
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
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
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
|
-
|
398
|
+
yield direntry
|
936
399
|
elif direntry.is_dir():
|
937
400
|
try:
|
938
|
-
|
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:
|