dissect.target 3.19.dev40__py3-none-any.whl → 3.19.dev42__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,6 +34,89 @@ LnkRecord = TargetRecordDescriptor(
34
34
  )
35
35
 
36
36
 
37
+ def parse_lnk_file(target: Target, lnk_file: Lnk, lnk_path: TargetPath) -> Iterator[LnkRecord]:
38
+ # we need to get the active codepage from the system to properly decode some values
39
+ codepage = target.codepage or "ascii"
40
+
41
+ lnk_net_name = lnk_device_name = None
42
+
43
+ if lnk_file.link_header:
44
+ lnk_name = lnk_file.stringdata.name_string.string if lnk_file.flag("has_name") else None
45
+
46
+ lnk_mtime = ts.from_unix(lnk_path.stat().st_mtime)
47
+ lnk_atime = ts.from_unix(lnk_path.stat().st_atime)
48
+ lnk_ctime = ts.from_unix(lnk_path.stat().st_ctime)
49
+
50
+ lnk_relativepath = (
51
+ target.fs.path(lnk_file.stringdata.relative_path.string) if lnk_file.flag("has_relative_path") else None
52
+ )
53
+ lnk_workdir = (
54
+ target.fs.path(lnk_file.stringdata.working_dir.string) if lnk_file.flag("has_working_dir") else None
55
+ )
56
+ lnk_iconlocation = (
57
+ target.fs.path(lnk_file.stringdata.icon_location.string) if lnk_file.flag("has_icon_location") else None
58
+ )
59
+ lnk_arguments = lnk_file.stringdata.command_line_arguments.string if lnk_file.flag("has_arguments") else None
60
+ local_base_path = (
61
+ lnk_file.linkinfo.local_base_path.decode(codepage)
62
+ if lnk_file.flag("has_link_info") and lnk_file.linkinfo.flag("volumeid_and_local_basepath")
63
+ else None
64
+ )
65
+ common_path_suffix = (
66
+ lnk_file.linkinfo.common_path_suffix.decode(codepage) if lnk_file.flag("has_link_info") else None
67
+ )
68
+
69
+ if local_base_path and common_path_suffix:
70
+ lnk_full_path = target.fs.path(local_base_path + common_path_suffix)
71
+ elif local_base_path and not common_path_suffix:
72
+ lnk_full_path = target.fs.path(local_base_path)
73
+ else:
74
+ lnk_full_path = None
75
+
76
+ if lnk_file.flag("has_link_info"):
77
+ if lnk_file.linkinfo.flag("common_network_relative_link_and_pathsuffix"):
78
+ lnk_net_name = (
79
+ lnk_file.linkinfo.common_network_relative_link.net_name.decode()
80
+ if lnk_file.linkinfo.common_network_relative_link.net_name
81
+ else None
82
+ )
83
+ lnk_device_name = (
84
+ lnk_file.linkinfo.common_network_relative_link.device_name.decode()
85
+ if lnk_file.linkinfo.common_network_relative_link.device_name
86
+ else None
87
+ )
88
+ try:
89
+ machine_id = lnk_file.extradata.TRACKER_PROPS.machine_id.decode(codepage).strip("\x00")
90
+ except AttributeError:
91
+ machine_id = None
92
+
93
+ target_mtime = ts.wintimestamp(lnk_file.link_header.write_time)
94
+ target_atime = ts.wintimestamp(lnk_file.link_header.access_time)
95
+ target_ctime = ts.wintimestamp(lnk_file.link_header.creation_time)
96
+
97
+ return LnkRecord(
98
+ lnk_path=lnk_path,
99
+ lnk_name=lnk_name,
100
+ lnk_mtime=lnk_mtime,
101
+ lnk_atime=lnk_atime,
102
+ lnk_ctime=lnk_ctime,
103
+ lnk_relativepath=lnk_relativepath,
104
+ lnk_workdir=lnk_workdir,
105
+ lnk_iconlocation=lnk_iconlocation,
106
+ lnk_arguments=lnk_arguments,
107
+ local_base_path=local_base_path,
108
+ common_path_suffix=common_path_suffix,
109
+ lnk_full_path=lnk_full_path,
110
+ lnk_net_name=lnk_net_name,
111
+ lnk_device_name=lnk_device_name,
112
+ machine_id=machine_id,
113
+ target_mtime=target_mtime,
114
+ target_atime=target_atime,
115
+ target_ctime=target_ctime,
116
+ _target=target,
117
+ )
118
+
119
+
37
120
  class LnkPlugin(Plugin):
38
121
  def __init__(self, target: Target) -> None:
39
122
  super().__init__(target)
@@ -74,97 +157,9 @@ class LnkPlugin(Plugin):
74
157
  target_ctime (datetime): Creation time of the target (linked) file.
75
158
  """
76
159
 
77
- # we need to get the active codepage from the system to properly decode some values
78
- codepage = self.target.codepage or "ascii"
79
-
80
160
  for entry in self.lnk_entries(path):
81
161
  lnk_file = Lnk(entry.open())
82
- lnk_net_name = lnk_device_name = None
83
-
84
- if lnk_file.link_header:
85
- lnk_path = entry
86
- lnk_name = lnk_file.stringdata.name_string.string if lnk_file.flag("has_name") else None
87
-
88
- lnk_mtime = ts.from_unix(entry.stat().st_mtime)
89
- lnk_atime = ts.from_unix(entry.stat().st_atime)
90
- lnk_ctime = ts.from_unix(entry.stat().st_ctime)
91
-
92
- lnk_relativepath = (
93
- self.target.fs.path(lnk_file.stringdata.relative_path.string)
94
- if lnk_file.flag("has_relative_path")
95
- else None
96
- )
97
- lnk_workdir = (
98
- self.target.fs.path(lnk_file.stringdata.working_dir.string)
99
- if lnk_file.flag("has_working_dir")
100
- else None
101
- )
102
- lnk_iconlocation = (
103
- self.target.fs.path(lnk_file.stringdata.icon_location.string)
104
- if lnk_file.flag("has_icon_location")
105
- else None
106
- )
107
- lnk_arguments = (
108
- lnk_file.stringdata.command_line_arguments.string if lnk_file.flag("has_arguments") else None
109
- )
110
- local_base_path = (
111
- lnk_file.linkinfo.local_base_path.decode(codepage)
112
- if lnk_file.flag("has_link_info") and lnk_file.linkinfo.flag("volumeid_and_local_basepath")
113
- else None
114
- )
115
- common_path_suffix = (
116
- lnk_file.linkinfo.common_path_suffix.decode(codepage) if lnk_file.flag("has_link_info") else None
117
- )
118
-
119
- if local_base_path and common_path_suffix:
120
- lnk_full_path = self.target.fs.path(local_base_path + common_path_suffix)
121
- elif local_base_path and not common_path_suffix:
122
- lnk_full_path = self.target.fs.path(local_base_path)
123
- else:
124
- lnk_full_path = None
125
-
126
- if lnk_file.flag("has_link_info"):
127
- if lnk_file.linkinfo.flag("common_network_relative_link_and_pathsuffix"):
128
- lnk_net_name = (
129
- lnk_file.linkinfo.common_network_relative_link.net_name.decode()
130
- if lnk_file.linkinfo.common_network_relative_link.net_name
131
- else None
132
- )
133
- lnk_device_name = (
134
- lnk_file.linkinfo.common_network_relative_link.device_name.decode()
135
- if lnk_file.linkinfo.common_network_relative_link.device_name
136
- else None
137
- )
138
- try:
139
- machine_id = lnk_file.extradata.TRACKER_PROPS.machine_id.decode(codepage).strip("\x00")
140
- except AttributeError:
141
- machine_id = None
142
-
143
- target_mtime = ts.wintimestamp(lnk_file.link_header.write_time)
144
- target_atime = ts.wintimestamp(lnk_file.link_header.access_time)
145
- target_ctime = ts.wintimestamp(lnk_file.link_header.creation_time)
146
-
147
- yield LnkRecord(
148
- lnk_path=lnk_path,
149
- lnk_name=lnk_name,
150
- lnk_mtime=lnk_mtime,
151
- lnk_atime=lnk_atime,
152
- lnk_ctime=lnk_ctime,
153
- lnk_relativepath=lnk_relativepath,
154
- lnk_workdir=lnk_workdir,
155
- lnk_iconlocation=lnk_iconlocation,
156
- lnk_arguments=lnk_arguments,
157
- local_base_path=local_base_path,
158
- common_path_suffix=common_path_suffix,
159
- lnk_full_path=lnk_full_path,
160
- lnk_net_name=lnk_net_name,
161
- lnk_device_name=lnk_device_name,
162
- machine_id=machine_id,
163
- target_mtime=target_mtime,
164
- target_atime=target_atime,
165
- target_ctime=target_ctime,
166
- _target=self.target,
167
- )
162
+ yield parse_lnk_file(self.target, lnk_file, entry)
168
163
 
169
164
  def lnk_entries(self, path: Optional[str] = None) -> Iterator[TargetPath]:
170
165
  if path:
dissect/target/target.py CHANGED
@@ -87,7 +87,7 @@ class Target:
87
87
  self._applied = False
88
88
 
89
89
  try:
90
- self._config = config.load([self.path, os.getcwd()])
90
+ self._config = config.load([self.path, Path.cwd(), Path.home()])
91
91
  except Exception as e:
92
92
  self.log.warning("Error loading config file: %s", self.path)
93
93
  self.log.debug("", exc_info=e)
@@ -2,9 +2,7 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  import argparse
5
- import datetime
6
5
  import logging
7
- import operator
8
6
  import os
9
7
  import pathlib
10
8
  import shutil
@@ -13,7 +11,7 @@ import sys
13
11
  from dissect.target import Target
14
12
  from dissect.target.exceptions import TargetError
15
13
  from dissect.target.helpers.fsutil import TargetPath
16
- from dissect.target.tools.shell import stat_modestr
14
+ from dissect.target.tools.fsutils import print_ls, print_stat
17
15
  from dissect.target.tools.utils import (
18
16
  catch_sigpipe,
19
17
  configure_generic_arguments,
@@ -25,11 +23,6 @@ logging.lastResort = None
25
23
  logging.raiseExceptions = False
26
24
 
27
25
 
28
- def human_size(bytes: int, units: list[str] = ["", "K", "M", "G", "T", "P", "E"]) -> str:
29
- """Helper function to return the human readable string representation of bytes."""
30
- return str(bytes) + units[0] if bytes < 1024 else human_size(bytes >> 10, units[1:])
31
-
32
-
33
26
  def ls(t: Target, path: TargetPath, args: argparse.Namespace) -> None:
34
27
  if args.use_ctime and args.use_atime:
35
28
  log.error("Can't specify -c and -u at the same time")
@@ -37,63 +30,20 @@ def ls(t: Target, path: TargetPath, args: argparse.Namespace) -> None:
37
30
  if not path or not path.exists():
38
31
  return
39
32
 
40
- _print_ls(args, path, 0)
41
-
42
-
43
- def _print_ls(args: argparse.Namespace, path: TargetPath, depth: int) -> None:
44
- subdirs = []
45
-
46
- if path.is_dir():
47
- contents = sorted(path.iterdir(), key=operator.attrgetter("name"))
48
- elif path.is_file():
49
- contents = [path]
50
-
51
- if depth > 0:
52
- print(f"\n{str(path)}:")
53
-
54
- if not args.l:
55
- for entry in contents:
56
- print(entry.name)
57
-
58
- if entry.is_dir():
59
- subdirs.append(entry)
60
- else:
61
- if len(contents) > 1:
62
- print(f"total {len(contents)}")
63
-
64
- for entry in contents:
65
- _print_extensive_file_stat(args, entry, entry.name)
66
-
67
- if entry.is_dir():
68
- subdirs.append(entry)
69
-
70
- if args.recursive and subdirs:
71
- for subdir in subdirs:
72
- _print_ls(args, subdir, depth + 1)
73
-
74
-
75
- def _print_extensive_file_stat(args: argparse.Namespace, path: TargetPath, name: str) -> None:
76
- try:
77
- entry = path.get()
78
- stat = entry.lstat()
79
- symlink = f" -> {entry.readlink()}" if entry.is_symlink() else ""
80
- show_time = stat.st_mtime
81
-
82
- if args.use_ctime:
83
- show_time = stat.st_ctime
84
- elif args.use_atime:
85
- show_time = stat.st_atime
86
-
87
- utc_time = datetime.datetime.utcfromtimestamp(show_time).isoformat()
88
-
89
- if args.human_readable:
90
- size = human_size(stat.st_size)
91
- else:
92
- size = stat.st_size
93
-
94
- print(f"{stat_modestr(stat)} {stat.st_uid:4d} {stat.st_gid:4d} {size:>6s} {utc_time} {name}{symlink}")
95
- except FileNotFoundError:
96
- print(f"?????????? ? ? ? ????-??-??T??:??:??.?????? {name}")
33
+ # Only output with colors if stdout is a tty
34
+ use_colors = sys.stdout.buffer.isatty()
35
+
36
+ print_ls(
37
+ path,
38
+ 0,
39
+ sys.stdout,
40
+ args.l,
41
+ args.human_readable,
42
+ args.recursive,
43
+ args.use_ctime,
44
+ args.use_atime,
45
+ use_colors,
46
+ )
97
47
 
98
48
 
99
49
  def cat(t: Target, path: TargetPath, args: argparse.Namespace) -> None:
@@ -120,6 +70,12 @@ def cp(t: Target, path: TargetPath, args: argparse.Namespace) -> None:
120
70
  print("[!] Failed, unsuported file type: %s" % path)
121
71
 
122
72
 
73
+ def stat(t: Target, path: TargetPath, args: argparse.Namespace) -> None:
74
+ if not path or not path.exists():
75
+ return
76
+ print_stat(path, sys.stdout, args.dereference)
77
+
78
+
123
79
  def _extract_path(path: TargetPath, output_path: str) -> None:
124
80
  print("%s -> %s" % (path, output_path))
125
81
 
@@ -172,6 +128,10 @@ def main() -> None:
172
128
  parser_cat = subparsers.add_parser("cat", help="dump file contents", parents=[baseparser])
173
129
  parser_cat.set_defaults(handler=cat)
174
130
 
131
+ parser_stat = subparsers.add_parser("stat", help="display file status", parents=[baseparser])
132
+ parser_stat.add_argument("-L", "--dereference", action="store_true")
133
+ parser_stat.set_defaults(handler=stat)
134
+
175
135
  parser_find = subparsers.add_parser("walk", help="perform a walk", parents=[baseparser])
176
136
  parser_find.set_defaults(handler=walk)
177
137
 
@@ -0,0 +1,243 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import stat
5
+ from datetime import datetime, timezone
6
+ from typing import TextIO
7
+
8
+ from dissect.target.exceptions import FileNotFoundError
9
+ from dissect.target.filesystem import FilesystemEntry, LayerFilesystemEntry
10
+ from dissect.target.helpers import fsutil
11
+ from dissect.target.helpers.fsutil import TargetPath
12
+
13
+ # ['mode', 'addr', 'dev', 'nlink', 'uid', 'gid', 'size', 'atime', 'mtime', 'ctime']
14
+ STAT_TEMPLATE = """ File: {path} {symlink}
15
+ Size: {size} Blocks: {blocks} IO Block: {blksize} {filetype}
16
+ Device: {device} Inode: {inode} Links: {nlink}
17
+ Access: ({modeord}/{modestr}) Uid: ( {uid} ) Gid: ( {gid} )
18
+ Access: {atime}
19
+ Modify: {mtime}
20
+ Change: {ctime}
21
+ Birth: {btime}"""
22
+
23
+ FALLBACK_LS_COLORS = "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32" # noqa: E501
24
+
25
+
26
+ def prepare_ls_colors() -> dict[str, str]:
27
+ """Parse the LS_COLORS environment variable so we can use it later."""
28
+ d = {}
29
+ ls_colors = os.environ.get("LS_COLORS", FALLBACK_LS_COLORS)
30
+ for line in ls_colors.split(":"):
31
+ if not line:
32
+ continue
33
+
34
+ ft, _, value = line.partition("=")
35
+ if ft.startswith("*"):
36
+ ft = ft[1:]
37
+
38
+ d[ft] = f"\x1b[{value}m{{}}\x1b[0m"
39
+
40
+ return d
41
+
42
+
43
+ LS_COLORS = prepare_ls_colors()
44
+
45
+
46
+ def fmt_ls_colors(ft: str, name: str) -> str:
47
+ """Helper method to colorize strings according to LS_COLORS."""
48
+ try:
49
+ return LS_COLORS[ft].format(name)
50
+ except KeyError:
51
+ pass
52
+
53
+ try:
54
+ return LS_COLORS[fsutil.splitext(name)[1]].format(name)
55
+ except KeyError:
56
+ pass
57
+
58
+ return name
59
+
60
+
61
+ def human_size(bytes: int, units: list[str] = ["", "K", "M", "G", "T", "P", "E"]) -> str:
62
+ """Helper function to return the human readable string representation of bytes."""
63
+ return str(bytes) + units[0] if bytes < 1024 else human_size(bytes >> 10, units[1:])
64
+
65
+
66
+ def stat_modestr(st: fsutil.stat_result) -> str:
67
+ """Helper method for generating a mode string from a numerical mode value."""
68
+ return stat.filemode(st.st_mode)
69
+
70
+
71
+ def print_extensive_file_stat_listing(
72
+ stdout: TextIO,
73
+ name: str,
74
+ entry: FilesystemEntry | None = None,
75
+ timestamp: datetime | None = None,
76
+ human_readable: bool = False,
77
+ ) -> None:
78
+ """Print the file status as a single line."""
79
+ if entry is not None:
80
+ try:
81
+ entry_stat = entry.lstat()
82
+ if timestamp is None:
83
+ timestamp = entry_stat.st_mtime
84
+ symlink = f" -> {entry.readlink()}" if entry.is_symlink() else ""
85
+ utc_time = datetime.fromtimestamp(timestamp, tz=timezone.utc).isoformat(timespec="microseconds")
86
+ size = f"{human_size(entry_stat.st_size):5s}" if human_readable else f"{entry_stat.st_size:10d}"
87
+
88
+ print(
89
+ (
90
+ f"{stat_modestr(entry_stat)} {entry_stat.st_uid:4d} {entry_stat.st_gid:4d} {size} "
91
+ f"{utc_time} {name}{symlink}"
92
+ ),
93
+ file=stdout,
94
+ )
95
+ return
96
+ except FileNotFoundError:
97
+ pass
98
+
99
+ hr_spaces = f"{'':5s}" if human_readable else " "
100
+ regular_spaces = f"{'':10s}" if not human_readable else " "
101
+
102
+ print(f"?????????? ? ?{regular_spaces}?{hr_spaces}????-??-??T??:??:??.??????+??:?? {name}", file=stdout)
103
+
104
+
105
+ def ls_scandir(path: fsutil.TargetPath, color: bool = False) -> list[tuple[fsutil.TargetPath, str]]:
106
+ """List a directory for the given path."""
107
+ result = []
108
+ if not path.exists() or not path.is_dir():
109
+ return []
110
+
111
+ for file_ in path.iterdir():
112
+ file_type = None
113
+ if color:
114
+ if file_.is_symlink():
115
+ file_type = "ln"
116
+ elif file_.is_dir():
117
+ file_type = "di"
118
+ elif file_.is_file():
119
+ file_type = "fi"
120
+
121
+ result.append((file_, fmt_ls_colors(file_type, file_.name) if color else file_.name))
122
+
123
+ # If we happen to scan an NTFS filesystem see if any of the
124
+ # entries has an alternative data stream and also list them.
125
+ entry = file_.get()
126
+ if isinstance(entry, LayerFilesystemEntry):
127
+ if entry.entries.fs.__type__ == "ntfs":
128
+ attrs = entry.lattr()
129
+ for data_stream in attrs.DATA:
130
+ if data_stream.name != "":
131
+ name = f"{file_.name}:{data_stream.name}"
132
+ result.append((file_, fmt_ls_colors(file_type, name) if color else name))
133
+
134
+ result.sort(key=lambda e: e[0].name)
135
+
136
+ return result
137
+
138
+
139
+ def print_ls(
140
+ path: fsutil.TargetPath,
141
+ depth: int,
142
+ stdout: TextIO,
143
+ long_listing: bool = False,
144
+ human_readable: bool = False,
145
+ recursive: bool = False,
146
+ use_ctime: bool = False,
147
+ use_atime: bool = False,
148
+ color: bool = True,
149
+ ) -> None:
150
+ """Print ls output"""
151
+ subdirs = []
152
+
153
+ if path.is_dir():
154
+ contents = ls_scandir(path, color)
155
+ elif path.is_file():
156
+ contents = [(path, path.name)]
157
+
158
+ if depth > 0:
159
+ print(f"\n{str(path)}:", file=stdout)
160
+
161
+ if not long_listing:
162
+ for target_path, name in contents:
163
+ print(name, file=stdout)
164
+ if target_path.is_dir():
165
+ subdirs.append(target_path)
166
+ else:
167
+ if len(contents) > 1:
168
+ print(f"total {len(contents)}", file=stdout)
169
+ for target_path, name in contents:
170
+ try:
171
+ entry = target_path.get()
172
+ entry_stat = entry.lstat()
173
+ show_time = entry_stat.st_mtime
174
+ if use_ctime:
175
+ show_time = entry_stat.st_ctime
176
+ elif use_atime:
177
+ show_time = entry_stat.st_atime
178
+ except FileNotFoundError:
179
+ entry = None
180
+ show_time = None
181
+ print_extensive_file_stat_listing(stdout, name, entry, show_time, human_readable)
182
+ if target_path.is_dir():
183
+ subdirs.append(target_path)
184
+
185
+ if recursive and subdirs:
186
+ for subdir in subdirs:
187
+ print_ls(subdir, depth + 1, stdout, long_listing, human_readable, recursive, use_ctime, use_atime, color)
188
+
189
+
190
+ def print_stat(path: fsutil.TargetPath, stdout: TextIO, dereference: bool = False) -> None:
191
+ """Print file status."""
192
+ symlink = f"-> {path.readlink()}" if path.is_symlink() else ""
193
+ s = path.stat() if dereference else path.lstat()
194
+
195
+ def filetype(path: TargetPath) -> str:
196
+ if path.is_dir():
197
+ return "directory"
198
+ elif path.is_symlink():
199
+ return "symbolic link"
200
+ elif path.is_file():
201
+ return "regular file"
202
+
203
+ res = STAT_TEMPLATE.format(
204
+ path=path,
205
+ symlink=symlink,
206
+ size=s.st_size,
207
+ filetype=filetype(path),
208
+ device="?",
209
+ inode=s.st_ino,
210
+ blocks=s.st_blocks or "?",
211
+ blksize=s.st_blksize or "?",
212
+ nlink=s.st_nlink,
213
+ modeord=oct(stat.S_IMODE(s.st_mode)),
214
+ modestr=stat_modestr(s),
215
+ uid=s.st_uid,
216
+ gid=s.st_gid,
217
+ atime=datetime.fromtimestamp(s.st_atime, tz=timezone.utc).isoformat(timespec="microseconds"),
218
+ mtime=datetime.fromtimestamp(s.st_mtime, tz=timezone.utc).isoformat(timespec="microseconds"),
219
+ ctime=datetime.fromtimestamp(s.st_ctime, tz=timezone.utc).isoformat(timespec="microseconds"),
220
+ btime=datetime.fromtimestamp(s.st_birthtime, tz=timezone.utc).isoformat(timespec="microseconds")
221
+ if hasattr(s, "st_birthtime") and s.st_birthtime
222
+ else "?",
223
+ )
224
+ print(res, file=stdout)
225
+
226
+ try:
227
+ if (xattr := path.get().attr()) and isinstance(xattr, list) and hasattr(xattr[0], "name"):
228
+ print(" Attr:")
229
+ print_xattr(path.name, xattr, stdout)
230
+ except Exception:
231
+ pass
232
+
233
+
234
+ def print_xattr(basename: str, xattr: list, stdout: TextIO) -> None:
235
+ """Mimics getfattr -d {file} behaviour."""
236
+ if not hasattr(xattr[0], "name"):
237
+ return
238
+
239
+ XATTR_TEMPLATE = "# file: {basename}\n{attrs}"
240
+ res = XATTR_TEMPLATE.format(
241
+ basename=basename, attrs="\n".join([f'{attr.name}="{attr.value.decode()}"' for attr in xattr])
242
+ )
243
+ print(res, file=stdout)
@@ -4,6 +4,7 @@
4
4
  import argparse
5
5
  import json
6
6
  import logging
7
+ from datetime import datetime
7
8
  from pathlib import Path
8
9
  from typing import Union
9
10
 
@@ -138,10 +139,13 @@ def print_target_info(target: Target) -> None:
138
139
  if isinstance(value, list):
139
140
  value = ", ".join(value)
140
141
 
142
+ if isinstance(value, datetime):
143
+ value = value.isoformat(timespec="microseconds")
144
+
141
145
  if name == "hostname":
142
146
  print()
143
147
 
144
- print(f"{name.capitalize().replace('_', ' ')}" + (14 - len(name)) * " " + f" : {value}")
148
+ print(f"{name.capitalize().replace('_', ' '):14s} : {value}")
145
149
 
146
150
 
147
151
  def get_disks_info(target: Target) -> list[dict[str, Union[str, int]]]: