Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions dissect/target/tools/fs.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,17 @@ def ls(t: Target, path: TargetPath, args: argparse.Namespace) -> None:
use_colors = sys.stdout.buffer.isatty()

print_ls(
path,
0,
sys.stdout,
args.l,
args.human_readable,
args.recursive,
args.use_ctime,
args.use_atime,
use_colors,
path=path,
depth=0,
stdout=sys.stdout,
long_listing=args.l,
human_readable=args.human_readable,
recursive=args.recursive,
use_ctime=args.use_ctime,
use_atime=args.use_atime,
sort_by_time=args.sort_by_time,
reverse_sort=args.reverse_sort,
color=use_colors,
)


Expand Down Expand Up @@ -124,6 +126,8 @@ def main() -> int:
"-c", action="store_true", dest="use_ctime", help="show time when file status was last changed"
)
parser_ls.add_argument("-u", action="store_true", dest="use_atime", help="show time of last access")
parser_ls.add_argument("-t", action="store_true", dest="sort_by_time", help="sort by time (newest first)")
parser_ls.add_argument("-r", action="store_true", dest="reverse_sort", help="reverse the sort order")
parser_ls.set_defaults(handler=ls)

parser_cat = subparsers.add_parser("cat", help="dump file contents", parents=[baseparser])
Expand Down
26 changes: 15 additions & 11 deletions dissect/target/tools/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,12 +652,12 @@ def completedefault(self, text: str, line: str, begidx: int, endidx: int) -> lis
textlower = text.lower()

suggestions = []
for fpath, fname in ls_scandir(path):
if not fname.lower().startswith(textlower):
for entry in ls_scandir(path):
if not entry.name.lower().startswith(textlower):
continue

# Add a trailing slash to directories, to allow for easier traversal of the filesystem
suggestion = f"{fname}/" if fpath.is_dir() else fname
suggestion = f"{entry.name}/" if entry.path.is_dir() else entry.name
suggestions.append(suggestion)
return suggestions

Expand Down Expand Up @@ -793,6 +793,8 @@ def do_reload(self, line: str) -> bool:
@arg("-R", "--recursive", action="store_true", help="recursively list subdirectories encountered")
@arg("-c", action="store_true", dest="use_ctime", help="show time when file status was last changed")
@arg("-u", action="store_true", dest="use_atime", help="show time of last access")
@arg("-t", action="store_true", dest="sort_by_time", help="sort by time, newest first")
@arg("-r", action="store_true", dest="reverse_sort", help="reverse sort order")
@alias("l")
@alias("dir")
def cmd_ls(self, args: argparse.Namespace, stdout: TextIO) -> bool:
Expand All @@ -819,14 +821,16 @@ def cmd_ls(self, args: argparse.Namespace, stdout: TextIO) -> bool:
use_color = False

print_ls(
path,
0,
stdout,
args.l,
args.human_readable,
args.recursive,
args.use_ctime,
args.use_atime,
path=path,
depth=0,
stdout=stdout,
long_listing=args.l,
human_readable=args.human_readable,
recursive=args.recursive,
use_ctime=args.use_ctime,
use_atime=args.use_atime,
sort_by_time=args.sort_by_time,
reverse_sort=args.reverse_sort,
color=use_color,
)
return False
Expand Down
97 changes: 75 additions & 22 deletions dissect/target/tools/utils/fs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os
import stat
from datetime import datetime, timezone
from typing import TYPE_CHECKING, TextIO
from typing import TYPE_CHECKING, NamedTuple, TextIO

from dissect.target.exceptions import FileNotFoundError
from dissect.target.filesystem import FilesystemEntry, LayerFilesystemEntry
Expand All @@ -14,6 +14,14 @@

from dissect.target.helpers.fsutil import TargetPath


class ScanDirEntry(NamedTuple):
path: TargetPath
name: str
entry: FilesystemEntry | None = None
lstat: fsutil.stat_result | None = None


# ['mode', 'addr', 'dev', 'nlink', 'uid', 'gid', 'size', 'atime', 'mtime', 'ctime']
STAT_TEMPLATE = """ File: {path} {symlink}
Size: {size} Blocks: {blocks} IO Block: {blksize} {filetype}
Expand Down Expand Up @@ -106,7 +114,7 @@ def print_extensive_file_stat_listing(
print(f"?????????? ? ?{regular_spaces}?{hr_spaces}????-??-??T??:??:??.??????+??:?? {name}", file=stdout)


def ls_scandir(path: fsutil.TargetPath, color: bool = False) -> list[tuple[fsutil.TargetPath, str]]:
def ls_scandir(path: fsutil.TargetPath, *, color: bool = False) -> list[ScanDirEntry]:
"""List a directory for the given path."""
result = []
if not path.exists() or not path.is_dir():
Expand All @@ -122,7 +130,14 @@ def ls_scandir(path: fsutil.TargetPath, color: bool = False) -> list[tuple[fsuti
elif file_.is_file():
file_type = "fi"

result.append((file_, fmt_ls_colors(file_type, file_.name) if color else file_.name))
result.append(
ScanDirEntry(
file_,
fmt_ls_colors(file_type, file_.name) if color else file_.name,
file_.get(),
file_.lstat(),
)
)

# If we happen to scan an NTFS filesystem see if any of the
# entries has an alternative data stream and also list them.
Expand All @@ -132,14 +147,22 @@ def ls_scandir(path: fsutil.TargetPath, color: bool = False) -> list[tuple[fsuti
for data_stream in attrs.DATA:
if data_stream.name != "":
name = f"{file_.name}:{data_stream.name}"
result.append((file_, fmt_ls_colors(file_type, name) if color else name))
result.append(
ScanDirEntry(
file_,
fmt_ls_colors(file_type, name) if color else name,
file_.get(),
file_.lstat(),
)
)

result.sort(key=lambda e: e[0].name)
result.sort(key=lambda e: e.name)

return result


def print_ls(
*,
path: fsutil.TargetPath,
depth: int,
stdout: TextIO,
Expand All @@ -148,46 +171,76 @@ def print_ls(
recursive: bool = False,
use_ctime: bool = False,
use_atime: bool = False,
sort_by_time: bool = False,
reverse_sort: bool = False,
color: bool = True,
) -> None:
"""Print ls output."""
subdirs = []

if path.is_dir():
contents = ls_scandir(path, color)
elif path.is_file():
contents = [(path, path.name)]
contents = ls_scandir(path, color=color)
else:
contents = [ScanDirEntry(path, path.name, path.get(), path.lstat())]

if sort_by_time:

def sort_key(e: ScanDirEntry) -> tuple[float, str]:
try:
show_time = e.lstat.st_mtime
if use_ctime:
show_time = e.lstat.st_ctime
elif use_atime:
show_time = e.lstat.st_atime
except FileNotFoundError:
show_time = 0
return show_time, e.name

contents.sort(key=sort_key, reverse=True)

if reverse_sort:
contents.reverse()

if depth > 0:
print(f"\n{path!s}:", file=stdout)

if not long_listing:
for target_path, name in contents:
print(name, file=stdout)
if not target_path.is_symlink() and target_path.is_dir():
subdirs.append(target_path)
for entry in contents:
print(entry.name, file=stdout)
if not entry.path.is_symlink() and entry.path.is_dir():
subdirs.append(entry.path)
else:
if len(contents) > 1:
print(f"total {len(contents)}", file=stdout)
for target_path, name in contents:
for entry in contents:
try:
entry = target_path.get()
entry_stat = entry.lstat()
show_time = entry_stat.st_mtime
show_time = entry.lstat.st_mtime
if use_ctime:
show_time = entry_stat.st_ctime
show_time = entry.lstat.st_ctime
elif use_atime:
show_time = entry_stat.st_atime
show_time = entry.lstat.st_atime
except FileNotFoundError:
entry = None
show_time = None
print_extensive_file_stat_listing(stdout, name, entry, show_time, human_readable)
if target_path.is_dir():
subdirs.append(target_path)
print_extensive_file_stat_listing(stdout, entry.name, entry.entry, show_time, human_readable)
if entry.path.is_dir():
subdirs.append(entry.path)

if recursive and subdirs:
for subdir in subdirs:
print_ls(subdir, depth + 1, stdout, long_listing, human_readable, recursive, use_ctime, use_atime, color)
print_ls(
path=subdir,
depth=depth + 1,
stdout=stdout,
long_listing=long_listing,
human_readable=human_readable,
recursive=recursive,
use_ctime=use_ctime,
use_atime=use_atime,
reverse_sort=reverse_sort,
sort_by_time=sort_by_time,
color=color,
)


def print_stat(path: fsutil.TargetPath, stdout: TextIO, dereference: bool = False) -> None:
Expand Down
8 changes: 4 additions & 4 deletions tests/tools/test_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,12 @@ def test_targetcli_autocomplete(target_bare: Target, monkeypatch: pytest.MonkeyP
subfile_name = "subfile"
subpath_mismatch = "mismatch"

def dummy_scandir(path: TargetPath) -> list[tuple[TargetPath | None, str]]:
def dummy_scandir(path: TargetPath) -> list[fs.ScanDirEntry]:
assert str(path) == base_path
return [
(mock_subfolder, subfolder_name),
(mock_subfile, subfile_name),
(None, subpath_mismatch),
fs.ScanDirEntry(mock_subfolder, subfolder_name),
fs.ScanDirEntry(mock_subfile, subfile_name),
fs.ScanDirEntry(None, subpath_mismatch),
]

monkeypatch.setattr("dissect.target.tools.shell.ls_scandir", dummy_scandir)
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ commands =
package = skip
dependency_groups = lint
commands =
ruff check --fix dissect tests
ruff format dissect tests
ruff check --fix dissect tests

[testenv:lint]
package = skip
Expand Down
Loading