diff --git a/dissect/ntfs/attr.py b/dissect/ntfs/attr.py index 6c2a09a..d9350fa 100644 --- a/dissect/ntfs/attr.py +++ b/dissect/ntfs/attr.py @@ -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``.""" @@ -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: @@ -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) + + 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``.""" diff --git a/dissect/ntfs/c_ntfs.py b/dissect/ntfs/c_ntfs.py index cadee16..5a6679f 100644 --- a/dissect/ntfs/c_ntfs.py +++ b/dissect/ntfs/c_ntfs.py @@ -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, diff --git a/dissect/ntfs/c_ntfs.pyi b/dissect/ntfs/c_ntfs.pyi index 1253ea9..4b5877d 100644 --- a/dissect/ntfs/c_ntfs.pyi +++ b/dissect/ntfs/c_ntfs.pyi @@ -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 = ... diff --git a/dissect/ntfs/index.py b/dissect/ntfs/index.py index f664523..ea53630 100644 --- a/dissect/ntfs/index.py +++ b/dissect/ntfs/index.py @@ -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: diff --git a/dissect/ntfs/mft.py b/dissect/ntfs/mft.py index dc4a517..00b3fef 100644 --- a/dissect/ntfs/mft.py +++ b/dissect/ntfs/mft.py @@ -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(