Skip to content
Merged
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
57 changes: 56 additions & 1 deletion dissect/ntfs/attr.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,11 @@ def last_access_time_ns(self) -> int:
"""Return the ``$FILE_NAME`` file ``LastAccessTime`` in nanoseconds."""
return ts_to_ns(self.attr.LastAccessTime)

@property
def allocated_size(self) -> int:
"""Return the ``$FILE_NAME`` file ``AllocatedLength``."""
return self.attr.AllocatedLength

@property
def file_size(self) -> int:
"""Return the ``$FILE_NAME`` file ``FileSize``."""
Expand All @@ -463,7 +468,16 @@ def file_size(self) -> int:
@property
def file_attributes(self) -> int:
"""Return the ``$FILE_NAME`` file ``FileAttributes``."""
return self.attr.FileAttributes
attributes = self.attr.FileAttributes

if attributes & c_ntfs.FILE_NAME_INDEX_PRESENT:
attributes &= ~c_ntfs.FILE_NAME_INDEX_PRESENT
attributes |= c_ntfs.FILE_ATTRIBUTE.DIRECTORY.value

if attributes == 0:
attributes |= c_ntfs.FILE_ATTRIBUTE.NORMAL.value

return c_ntfs.FILE_ATTRIBUTE(attributes)

@property
def flags(self) -> int:
Expand All @@ -479,6 +493,47 @@ def full_path(self) -> str:
"""Use the parent directory reference to try to generate a full path from this file name."""
return get_full_path(self.record.ntfs.mft, self.file_name, self.attr.ParentDirectory)

def is_dir(self) -> bool:
"""Return whether this ``$FILE_NAME`` attribute represents a directory."""
return bool(self.attr.FileAttributes & c_ntfs.FILE_NAME_INDEX_PRESENT)
Copy link
Contributor

@Miauwkeru Miauwkeru Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With file_attribute above in mind, wouldn't using the resulting value and checking whether the attribute DIRECTORY exists be more logical? With the question: What if FILE_ATTRIBUTE.DIRECTORY is set and not FILE_NAME_INDEX_PRESENT?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is faster.

And FILE_ATTRIBUTE.DIRECTORY is not a legal flag in this structure.


def is_file(self) -> bool:
"""Return whether this ``$FILE_NAME`` attribute represents a file."""
return not self.is_dir()

def is_reparse_point(self) -> bool:
"""Return whether this ``$FILE_NAME`` attribute represents a reparse point."""
return bool(self.attr.FileAttributes & c_ntfs.FILE_ATTRIBUTE.REPARSE_POINT)

def is_symlink(self) -> bool:
"""Return whether this ``$FILE_NAME`` attribute represents a symlink reparse point."""
return self.is_reparse_point() and self.attr.ReparsePointTag == IO_REPARSE_TAG.SYMLINK

def is_mount_point(self) -> bool:
"""Return whether this ``$FILE_NAME`` attribute represents a mount point reparse point."""
return self.is_reparse_point() and self.attr.ReparsePointTag == IO_REPARSE_TAG.MOUNT_POINT

def is_cloud_file(self) -> bool:
"""Return whether this ``$FILE_NAME`` attribute represents a cloud file."""
return self.is_reparse_point() and self.attr.ReparsePointTag in (
IO_REPARSE_TAG.CLOUD,
IO_REPARSE_TAG.CLOUD_1,
IO_REPARSE_TAG.CLOUD_2,
IO_REPARSE_TAG.CLOUD_3,
IO_REPARSE_TAG.CLOUD_4,
IO_REPARSE_TAG.CLOUD_5,
IO_REPARSE_TAG.CLOUD_6,
IO_REPARSE_TAG.CLOUD_7,
IO_REPARSE_TAG.CLOUD_8,
IO_REPARSE_TAG.CLOUD_9,
IO_REPARSE_TAG.CLOUD_A,
IO_REPARSE_TAG.CLOUD_B,
IO_REPARSE_TAG.CLOUD_C,
IO_REPARSE_TAG.CLOUD_D,
IO_REPARSE_TAG.CLOUD_E,
IO_REPARSE_TAG.CLOUD_F,
)


class ReparsePoint(AttributeRecord):
"""Specific :class:`AttributeRecord` parser for ``$REPARSE_POINT``."""
Expand Down
2 changes: 2 additions & 0 deletions dissect/ntfs/c_ntfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@
WCHAR FileName[FileNameLength];
} FILE_NAME;

#define FILE_NAME_INDEX_PRESENT 0x10000000

enum IO_REPARSE_TAG : ULONG {
RESERVED_ZERO = 0x00000000,
RESERVED_ONE = 0x00000001,
Expand Down
1 change: 1 addition & 0 deletions dissect/ntfs/c_ntfs.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ class _c_ntfs(__cs__.cstruct):
def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...

FILE_NAME: TypeAlias = _FILE_NAME
FILE_NAME_INDEX_PRESENT: Literal[0x10000000] = ...
class IO_REPARSE_TAG(__cs__.Enum):
RESERVED_ZERO = ...
RESERVED_ONE = ...
Expand Down
1 change: 1 addition & 0 deletions dissect/ntfs/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Index:
"""Open an index with he given name on the given MFT record.

Args:
record: The :class:`MftRecord` to open the index on.
name: The index to open.

Raises:
Expand Down
3 changes: 3 additions & 0 deletions dissect/ntfs/mft.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,9 @@ def reparse_point_record(self) -> MftRecord:
if reparse_point.relative:
target_name = ntpath.join(ntpath.dirname(self.full_path()), target_name)

if not target_name:
raise NotAReparsePointError(f"{self!r} does not have a valid reparse target")

return self.ntfs.mft.get(target_name)

def _get_stream_attributes(
Expand Down