Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add birthtime, blocksize and blks to stat_output for NTFSFilesystemEntry #848

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
19 changes: 17 additions & 2 deletions dissect/target/filesystems/ntfs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import math
import stat
from typing import BinaryIO, Iterator, Optional

Expand Down Expand Up @@ -176,16 +177,30 @@ def lstat(self) -> fsutil.stat_result:
0,
size,
stdinfo.last_access_time.timestamp(),
stdinfo.last_change_time.timestamp(),
stdinfo.last_modification_time.timestamp(),
# ctime gets set to creation time for python <3.12 purposes
stdinfo.creation_time.timestamp(),
]
)

# Set the nanosecond resolution separately
st_info.st_atime_ns = stdinfo.last_access_time_ns
st_info.st_mtime_ns = stdinfo.last_change_time_ns
st_info.st_mtime_ns = stdinfo.last_modification_time_ns

st_info.st_ctime_ns = stdinfo.creation_time_ns

st_info.st_birthtime = stdinfo.creation_time.timestamp()
st_info.st_birthtime_ns = stdinfo.creation_time_ns

st_info.st_blksize = record.ntfs.sector_size

blks = 0
# If the file is a resident, all its data is inside the MFT, so it technically
# doesn't use any additional blocks on the disk
if not record.resident:
blks = math.ceil(size / st_info.st_blksize)

st_info.st_blocks = blks
return st_info

def attr(self) -> AttributeMap:
Expand Down
72 changes: 69 additions & 3 deletions tests/filesystems/test_ntfs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import io
from unittest.mock import Mock, patch

import pytest
from dissect.ntfs.attr import Attribute, StandardInformation
from dissect.ntfs.c_ntfs import c_ntfs
from dissect.ntfs.exceptions import FileNotFoundError as NtfsFileNotFoundError
from dissect.ntfs.mft import MftRecord
from dissect.ntfs.util import AttributeMap

from dissect.target.exceptions import FileNotFoundError
Expand All @@ -17,7 +21,7 @@
("test:$hello:$test", "test:$hello", "$test"),
],
)
def test_ads_ntfs_filesystem(path, expected_path, expected_ads):
def test_ads_ntfs_filesystem(path: str, expected_path: str, expected_ads: str) -> None:
with patch("dissect.target.filesystems.ntfs.NTFS"):
filesystem = NtfsFilesystem()
entry = filesystem.get(path)
Expand All @@ -34,7 +38,7 @@ def test_ads_ntfs_filesystem(path, expected_path, expected_ads):
("ads", "test", "test"),
],
)
def test_ntfs_fileentry_open(ads, name, output):
def test_ntfs_fileentry_open(ads: str, name: str, output: str) -> None:
vfs = VirtualFilesystem()
mocked_entry = Mock()
mocked_entry.attributes = AttributeMap()
Expand All @@ -48,7 +52,7 @@ def test_ntfs_fileentry_open(ads, name, output):
mocked_entry.open.assert_called_once_with(output)


def test_ntfs_unknown_file():
def test_ntfs_unknown_file() -> None:
vfs = VirtualFilesystem()
mocked_entry = Mock()
mocked_entry.attributes = AttributeMap()
Expand All @@ -59,3 +63,65 @@ def test_ntfs_unknown_file():
entry = NtfsFilesystemEntry(vfs, "some/random/path", entry=mocked_entry)
with pytest.raises(FileNotFoundError):
entry.stat()


@pytest.mark.parametrize(
"sector_size, size, resident, expected_blks",
[
(0x1000, 0x343, False, 1),
(0x1000, 0x1001, False, 2),
(0x1000, 0, False, 0),
(0x1000, 0x2000, True, 0),
],
)
def test_stat_information(sector_size: int, size: int, resident: bool, expected_blks: int) -> None:
ntfs = Mock(sector_size=sector_size)

entry = MftRecord()
entry.header = c_ntfs._FILE_RECORD_SEGMENT_HEADER()
entry.ntfs = ntfs
entry.segment = 42

attribute_record = c_ntfs._ATTRIBUTE_RECORD_HEADER()
attribute_record.FormCode = 0 if resident else 1
if resident:
attribute_record.Form.Resident.ValueLength = size
else:
attribute_record.Form.Nonresident.FileSize = size

map = AttributeMap()
map[0x10] = StandardInformation(
io.BytesIO(
b"\xb5\xc3S\xbb\xd1a\xd8\x01\xc1H\xedc$\x04\xdb\x01d \x0c\xb0v\xcc\xd9"
b"\x01\xc1H\xedc$\x04\xdb\x01 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00%\x03\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x92h8\x00\x00\x00\x00"
)
)
map[0x80] = [Attribute.from_fh(io.BytesIO(attribute_record.dumps()))]

mock_fs = Mock()
with patch.object(entry, "attributes", map):
fs_entry = NtfsFilesystemEntry(mock_fs, "some/path", entry)

stat_info = fs_entry.lstat()

assert stat_info.st_mode == 33279
assert stat_info.st_ino == 42
assert stat_info.st_dev == id(mock_fs)
assert stat_info.st_nlink == 0
assert stat_info.st_uid == 0
assert stat_info.st_gid == 0
assert stat_info.st_size == size

assert stat_info.st_atime == 1726043227.939039
assert stat_info.st_atime_ns == 1726043227939040100
assert stat_info.st_mtime == 1726043227.939039
assert stat_info.st_mtime_ns == 1726043227939040100
assert stat_info.st_ctime == 1651900642.631773
assert stat_info.st_ctime_ns == 1651900642631774900
assert stat_info.st_birthtime == 1651900642.631773
assert stat_info.st_birthtime_ns == 1651900642631774900

assert stat_info.st_blksize == sector_size
assert stat_info.st_blocks == expected_blks