From 5fd5d97780b165fde534426fa4258eb2b129ab78 Mon Sep 17 00:00:00 2001 From: jleaniz Date: Fri, 19 Nov 2021 17:11:03 +0000 Subject: [PATCH 1/6] add initial support for index I30 records --- dtformats/indx_directory_entry.py | 144 +++++++++++++++++++++++ dtformats/indx_directory_entry.yml | 158 +++++++++++++++++++++++++ test_data/indx.records | Bin 0 -> 8192 bytes tests/indx_directory_entry.py | 183 +++++++++++++++++++++++++++++ 4 files changed, 485 insertions(+) create mode 100644 dtformats/indx_directory_entry.py create mode 100644 dtformats/indx_directory_entry.yml create mode 100644 test_data/indx.records create mode 100644 tests/indx_directory_entry.py diff --git a/dtformats/indx_directory_entry.py b/dtformats/indx_directory_entry.py new file mode 100644 index 0000000..955089f --- /dev/null +++ b/dtformats/indx_directory_entry.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +"""INDX entries """ + +import os + +from dtformats import data_format +from dtformats.errors import ParseError + +class INDXRecord(data_format.BinaryDataFile): + """ Class that represents an INDX record. + + Note: currently only supports $I30 index records + """ + + _FABRIC = data_format.BinaryDataFile.ReadDefinitionFile( + 'indx_directory_entry.yml') + + _DEBUG_INDX_ENTRY_HEADER = [ + ('signature', 'signature', ''), + ('fixup_value_offset', 'fixup_value_offset', '_FormatIntegerAsDecimal'), + ('num_fixup_values', 'num_fixup_values', '_FormatIntegerAsDecimal'), + ('logfile_sequence_number', 'logfile_sequence_number', + '_FormatIntegerAsDecimal'), + ('virtual_cluster_number', 'virtual_cluster_number', + '_FormatIntegerAsDecimal')] + + _DEBUG_INDX_NODE_HEADER = [ + ('index_values_offset', 'index_values_offset', + '_FormatIntegerAsDecimal'), + ('index_node_size', 'index_node_size', '_FormatIntegerAsDecimal'), + ('allocated_index_node_size', 'allocated_index_node_size', + '_FormatIntegerAsDecimal'), + ('index_node_flags', 'index_node_flags', '_FormatIntegerAsDecimal')] + + _DEBUG_INDX_DIR_RECORD = [ + ('file_reference', 'file_reference', '_FormatIntegerAsDecimal'), + ('index_value_size', 'index_value_size', '_FormatIntegerAsDecimal'), + ('index_key_data_size', 'index_key_data_size', '_FormatIntegerAsDecimal'), + ('index_value_flags', 'index_value_flags', '_FormatIntegerAsDecimal')] + + _DEBUG_FILE_NAME_ATTR = [ + ('parent_file_reference', 'parent_file_reference', + '_FormatIntegerAsDecimal'), + ('creation_time', 'creation_time', '_FormatIntegerAsDecimal'), + ('modification_time', 'modification_time', '_FormatIntegerAsDecimal'), + ('entry_modification_time', 'entry_modification_time', + '_FormatIntegerAsDecimal'), + ('access_time', 'access_time', '_FormatIntegerAsDecimal'), + ('allocated_file_size', 'allocated_file_size', '_FormatIntegerAsDecimal'), + ('file_size', 'file_size', '_FormatIntegerAsDecimal'), + ('file_attribute_flags', 'file_attribute_flags', + '_FormatIntegerAsDecimal'), + ('extended_data', 'extended_data', '_FormatIntegerAsDecimal'), + ('name_size', 'name_size', '_FormatIntegerAsDecimal'), + ('name_space', 'name_space', '_FormatIntegerAsDecimal'), + ('filename', 'filename', '_FormatString')] + + def PrintRecord(self, record): + """ + Prints a human readable version of the INDX record + to STDOUT. + + Args: + record (index_dir_entry): An index_dir_entry structure. + """ + if record is not None: + if self._debug: + self._DebugPrintStructureObject( + record.entry_header, self._DEBUG_INDX_ENTRY_HEADER) + self._DebugPrintStructureObject( + record.node_header, self._DEBUG_INDX_NODE_HEADER) + self._DebugPrintStructureObject( + record, self._DEBUG_INDX_DIR_RECORD) + self._DebugPrintStructureObject( + record.index_key_data, self._DEBUG_FILE_NAME_ATTR) + + + def _ParseIndexEntryHeader(self, file_object): + file_offset = file_object.tell() + data_type_map = self._GetDataTypeMap('index_entry_header') + + indx_record, data_size = self._ReadStructureFromFileObject( + file_object, file_offset, data_type_map, 'INDX Entry Header') + return indx_record, data_size + + + def _ParseIndexNodeHeader(self, file_object): + file_offset = file_object.tell() + data_type_map = self._GetDataTypeMap('index_node_header') + + indx_record, data_size = self._ReadStructureFromFileObject( + file_object, file_offset, data_type_map, 'INDX Node Header') + return indx_record, data_size + + + def _ParseIndexDirectoryEntry(self, file_object): + file_offset = file_object.tell() + data_type_map = self._GetDataTypeMap('index_dir_entry') + + try: + indx_record, data_size = self._ReadStructureFromFileObject( + file_object, file_offset, data_type_map, + '4KB block with possible INDX Directory Entry') + return indx_record, data_size + except ParseError as e: + if self._debug: + print(e) + return None, None + + + def ReadFileObject(self, file_object): + """Reads a file-like object containing INDX records. + + Args: + file_object (file): file-like object. + + Raises: + ParseError: if the file cannot be read. + """ + self._file_object = file_object + + def ReadRecords(self): + """ + Reads INDX records. + + Yields: + index_dir_entry: An $I30 INDX record. + + Raises: + ParseError: if a record cannot be read. + """ + self._file_object.seek(0, os.SEEK_SET) + file_offset = 0 + + # INDX entries allocated in 4096-byte chunks + block_size = 4096 + + while file_offset < self._file_size: + self._file_object.seek(file_offset, os.SEEK_SET) + index_dir_entry, _ = self._ParseIndexDirectoryEntry(self._file_object) + if index_dir_entry: + yield index_dir_entry + + file_offset += block_size diff --git a/dtformats/indx_directory_entry.yml b/dtformats/indx_directory_entry.yml new file mode 100644 index 0000000..e894811 --- /dev/null +++ b/dtformats/indx_directory_entry.yml @@ -0,0 +1,158 @@ +name: index_record +type: format +description: Index Directory Entry +urls: ["https://github.com/libyal/libfsntfs/blob/83c2f4ce3d16b5535eae9de767adc93fff724004/documentation/New%20Technologies%20File%20System%20(NTFS).asciidoc#index"] +metadata: + authors: ['Joachim Metz ', 'Juan Leaniz 64 + data_type: file_name_attribute diff --git a/test_data/indx.records b/test_data/indx.records new file mode 100644 index 0000000000000000000000000000000000000000..1d1240edf03c626d2e16dda8a030dc95ecb328f3 GIT binary patch literal 8192 zcmeI0U2IfE6vxlq4WVQ)V!{*AfVDtLQ&O6yHbf<*r5{2|g+_UetFJlnt^$iUOA7+pUKZP&c*V>NTUU z={}#``(-w0_&ss!mxtM4<_npQH-F0O)Psg3a-LD6(WTTx&Geq(^iUVBgnO;*I=I3U zDffj(aSasiRKs0JD`+LP())(rPpd5cyv;Yj7kS{s`~~|s4$(C=Ukkr1(0&Vl5iR5> z8tH@QY^RoEQFik2=)fgk;Z*s;fqG8MK{YuTxQ zKM~5%I$TLSdk*hK|Ll77s8aQd#3T2KN2o4Zik<1)|10?~POJIX$UogbagI%uxu^pk zK3W%hV|Qv`Z`SP#u;)rVW>+(mIwkrc4N*0ZW$pibvPvM9ApD{Up68vQ#S9$oS{%Qh$U;114?@ByoSB9UYEzEB%Z_;-)= zfdA>?o8Y_BMng0FJ{n+t3$^3s8V_H_y}Spm^z&ON&Dj~|QL0KE>TrHqp7c)I|3Ub# z=hyoGo!?K}ncv1SsM6r!A77t@|Gz!Hh5Wk`kJ**U{{ZKvnzOIxxy2s-lXIKEKX&#< z@Lg%6p&9;1cz@qE>)RX8;6I-IGx)Cb^EY$`>~;!v zdOrUZd{-LmcwzFCqF#=pljm+#8d3ZCr(<`)cO@RPs~lg=L!D;cZ}RYa+qS`fmn*uB zhGzH~%yV6auV>S+|7T7Pfj={K5B|IIX?!(KmzsWV_4uDjk4(T%kl1+^zstM_dNvCC zFZ@*Y0sMERpFfEAc$ImN^=jtlU#|TFd{_GUL$rhQvzljWRXR|I`Bz4FB0uGy{RzG+ z@t9qi{v9SY&&5st^(YPVE9!PnlE0JRS+$Sog!Okk{tI9BMXt16hsORVc%P#AZDt># zSIe;fvd*|%(I@7seURqIX|2^nB`?B1*=Id3zGbweI2o95|L3$Rsl}Ut1ht#<|GFAX zXSq<-`F}<ON7Kjhp%T;uWLsxDZ*pi_d31>9XXUHias9E=}{fPwz|T7@q7~ zgdhL=W$-UzeL*p#d3L=tbh94 zL%ICP_muydzp{wRUpJqB)VBxq6+#!ugKNhZzucGjMM@rAI}(pEhr_U1r;4-0iZiTI wR0U?B)c*x0sz{ra`oBm!w!pAT{a;|BinLj&|BJL^3k<8&{{<$hNSnpzA6M=d{r~^~ literal 0 HcmV?d00001 diff --git a/tests/indx_directory_entry.py b/tests/indx_directory_entry.py new file mode 100644 index 0000000..0eb735e --- /dev/null +++ b/tests/indx_directory_entry.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +"""Tests for NTFS INDX directory entries.""" + +import unittest +import os + +from dtformats import indx_directory_entry + +from tests import test_lib + + +class INDXRecordFileTest(test_lib.BaseTestCase): + """INDX record tests.""" + + expected_values = [ + {'entry_header_signature': b'INDX', + 'entry_header_fixup_value_offset': 40, + 'entry_header_num_fixup_values': 9, + 'entry_header_logfile_sequence_number': 1143279, + 'entry_header_virtual_cluster_number': 0, + 'node_header_index_values_offset': 40, + 'node_header_index_node_size': 3176, + 'node_header_allocated_index_node_size': 4072, + 'node_header_index_node_flags': 0, + 'file_reference' : 281474976710748, + 'index_value_size' : 88, + 'index_key_data_size' : 72, + 'index_value_flags' : 0, + 'index_key_data_parent_file_reference': 281474976710691, + 'index_key_data_creation_time': 130795153668593750, + 'index_key_data_modification_time': 130870367262995000, + 'index_key_data_entry_modification_time': 130849843578932500, + 'index_key_data_access_time': 130870367262995000, + 'index_key_data_allocated_file_size': 24576, + 'index_key_data_file_size': 24576, + 'index_key_data_file_attribute_flags': 38, + 'index_key_data_extended_data': 0, + 'index_key_data_name_size': 3, + 'index_key_data_name_space': 3, + 'index_key_data_filename': 'BCD', + }, + {'entry_header_signature': b'INDX', + 'entry_header_fixup_value_offset': 40, + 'entry_header_num_fixup_values': 9, + 'entry_header_logfile_sequence_number': 1060954, + 'entry_header_virtual_cluster_number': 0, + 'node_header_index_values_offset': 40, + 'node_header_index_node_size': 784, + 'node_header_allocated_index_node_size': 4072, + 'node_header_index_node_flags': 0, + 'file_reference' : 281474976710687, + 'index_value_size' : 96, + 'index_key_data_size' : 76, + 'index_value_flags' : 0, + 'index_key_data_parent_file_reference': 281474976710685, + 'index_key_data_creation_time': 130795138741093750, + 'index_key_data_modification_time': 130795138741093750, + 'index_key_data_entry_modification_time': 130795138741093750, + 'index_key_data_access_time': 130795138741093750, + 'index_key_data_allocated_file_size': 0, + 'index_key_data_file_size': 0, + 'index_key_data_file_attribute_flags': 38, + 'index_key_data_extended_data': 0, + 'index_key_data_name_size': 5, + 'index_key_data_name_space': 0, + 'index_key_data_filename': '$Tops', + }] + + + def _GetTestStructure(self, path): + test_file_path = self._GetTestFilePath([path]) + self._SkipIfPathNotExists(test_file_path) + + output_writer = test_lib.TestOutputWriter() + test_file = indx_directory_entry.INDXRecord( + debug=True, output_writer=output_writer) + + test_file.Open(test_file_path) + return test_file + + + def testReadFileObject(self): + """Tests the ReadFileObject function.""" + test_file = self._GetTestStructure('indx.records') + self.assertIsNotNone(test_file) + + + def testParseIndexEntryHeader(self): + """Tests the _ParseIndexEntryHeader function. """ + test_file = self._GetTestStructure('indx.records') + record, _ = test_file._ParseIndexEntryHeader(test_file._file_object) + + self.assertEqual(record.signature, + self.expected_values[0].get('entry_header_signature')) + self.assertEqual(record.fixup_value_offset, + self.expected_values[0].get('entry_header_fixup_value_offset')) + self.assertEqual(record.num_fixup_values, + self.expected_values[0].get('entry_header_num_fixup_values')) + self.assertEqual(record.logfile_sequence_number, + self.expected_values[0].get('entry_header_logfile_sequence_number')) + self.assertEqual(record.virtual_cluster_number, + self.expected_values[0].get('entry_header_virtual_cluster_number')) + + + def testParseIndexNodeHeader(self): + """Tests the _ParseIndexNodeHeader function. """ + test_file = self._GetTestStructure('indx.records') + test_file._file_object.seek(24, os.SEEK_SET) + record, _ = test_file._ParseIndexNodeHeader(test_file._file_object) + + self.assertEqual(record.index_values_offset, + self.expected_values[0].get('node_header_index_values_offset')) + self.assertEqual(record.index_node_size, + self.expected_values[0].get('node_header_index_node_size')) + self.assertEqual(record.allocated_index_node_size, + self.expected_values[0].get('node_header_allocated_index_node_size')) + self.assertEqual(record.index_node_flags, + self.expected_values[0].get('node_header_index_node_flags')) + + def testReadRecords(self): + """ + Method to test class functionality. + """ + test_file = self._GetTestStructure('indx.records') + + i = 0 + for r in test_file.ReadRecords(): + self.assertEqual(r.entry_header.signature, + self.expected_values[i].get('entry_header_signature')) + self.assertEqual(r.entry_header.fixup_value_offset, + self.expected_values[i].get('entry_header_fixup_value_offset')) + self.assertEqual(r.entry_header.num_fixup_values, + self.expected_values[i].get('entry_header_num_fixup_values')) + self.assertEqual(r.entry_header.logfile_sequence_number, + self.expected_values[i].get('entry_header_logfile_sequence_number')) + self.assertEqual(r.entry_header.virtual_cluster_number, + self.expected_values[i].get('entry_header_virtual_cluster_number')) + self.assertEqual(r.node_header.index_values_offset, + self.expected_values[i].get('node_header_index_values_offset')) + self.assertEqual(r.node_header.index_node_size, + self.expected_values[i].get('node_header_index_node_size')) + self.assertEqual(r.node_header.allocated_index_node_size, + self.expected_values[i].get('node_header_allocated_index_node_size')) + self.assertEqual(r.node_header.index_node_flags, + self.expected_values[i].get('node_header_index_node_flags')) + self.assertEqual(r.file_reference, + self.expected_values[i].get('file_reference')) + self.assertEqual(r.index_value_size, + self.expected_values[i].get('index_value_size')) + self.assertEqual(r.index_key_data_size, + self.expected_values[i].get('index_key_data_size')) + self.assertEqual(r.index_value_flags, + self.expected_values[i].get('index_value_flags')) + self.assertEqual(r.index_key_data.parent_file_reference, + self.expected_values[i].get('index_key_data_parent_file_reference')) + self.assertEqual(r.index_key_data.creation_time, + self.expected_values[i].get('index_key_data_creation_time')) + self.assertEqual(r.index_key_data.modification_time, + self.expected_values[i].get('index_key_data_modification_time')) + self.assertEqual(r.index_key_data.entry_modification_time, + self.expected_values[i].get('index_key_data_entry_modification_time')) + self.assertEqual(r.index_key_data.access_time, + self.expected_values[i].get('index_key_data_access_time')) + self.assertEqual(r.index_key_data.allocated_file_size, + self.expected_values[i].get('index_key_data_allocated_file_size')) + self.assertEqual(r.index_key_data.file_size, + self.expected_values[i].get('index_key_data_file_size')) + self.assertEqual(r.index_key_data.file_attribute_flags, + self.expected_values[i].get('index_key_data_file_attribute_flags')) + self.assertEqual(r.index_key_data.extended_data, + self.expected_values[i].get('index_key_data_extended_data')) + self.assertEqual(r.index_key_data.name_size, + self.expected_values[i].get('index_key_data_name_size')) + self.assertEqual(r.index_key_data.name_space, + self.expected_values[i].get('index_key_data_name_space')) + self.assertEqual(r.index_key_data.filename, + self.expected_values[i].get('index_key_data_filename')) + + i += 1 + + +if __name__ == '__main__': + unittest.main() From cd928909f48a3c9a29563269b5f22e4220bf34df Mon Sep 17 00:00:00 2001 From: jleaniz Date: Fri, 19 Nov 2021 17:15:55 +0000 Subject: [PATCH 2/6] add initial support for $I30 index records --- dtformats/indx_directory_entry.py | 144 +++++++++++++++++++++++ dtformats/indx_directory_entry.yml | 158 +++++++++++++++++++++++++ test_data/indx.records | Bin 0 -> 8192 bytes tests/indx_directory_entry.py | 183 +++++++++++++++++++++++++++++ 4 files changed, 485 insertions(+) create mode 100644 dtformats/indx_directory_entry.py create mode 100644 dtformats/indx_directory_entry.yml create mode 100644 test_data/indx.records create mode 100644 tests/indx_directory_entry.py diff --git a/dtformats/indx_directory_entry.py b/dtformats/indx_directory_entry.py new file mode 100644 index 0000000..955089f --- /dev/null +++ b/dtformats/indx_directory_entry.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +"""INDX entries """ + +import os + +from dtformats import data_format +from dtformats.errors import ParseError + +class INDXRecord(data_format.BinaryDataFile): + """ Class that represents an INDX record. + + Note: currently only supports $I30 index records + """ + + _FABRIC = data_format.BinaryDataFile.ReadDefinitionFile( + 'indx_directory_entry.yml') + + _DEBUG_INDX_ENTRY_HEADER = [ + ('signature', 'signature', ''), + ('fixup_value_offset', 'fixup_value_offset', '_FormatIntegerAsDecimal'), + ('num_fixup_values', 'num_fixup_values', '_FormatIntegerAsDecimal'), + ('logfile_sequence_number', 'logfile_sequence_number', + '_FormatIntegerAsDecimal'), + ('virtual_cluster_number', 'virtual_cluster_number', + '_FormatIntegerAsDecimal')] + + _DEBUG_INDX_NODE_HEADER = [ + ('index_values_offset', 'index_values_offset', + '_FormatIntegerAsDecimal'), + ('index_node_size', 'index_node_size', '_FormatIntegerAsDecimal'), + ('allocated_index_node_size', 'allocated_index_node_size', + '_FormatIntegerAsDecimal'), + ('index_node_flags', 'index_node_flags', '_FormatIntegerAsDecimal')] + + _DEBUG_INDX_DIR_RECORD = [ + ('file_reference', 'file_reference', '_FormatIntegerAsDecimal'), + ('index_value_size', 'index_value_size', '_FormatIntegerAsDecimal'), + ('index_key_data_size', 'index_key_data_size', '_FormatIntegerAsDecimal'), + ('index_value_flags', 'index_value_flags', '_FormatIntegerAsDecimal')] + + _DEBUG_FILE_NAME_ATTR = [ + ('parent_file_reference', 'parent_file_reference', + '_FormatIntegerAsDecimal'), + ('creation_time', 'creation_time', '_FormatIntegerAsDecimal'), + ('modification_time', 'modification_time', '_FormatIntegerAsDecimal'), + ('entry_modification_time', 'entry_modification_time', + '_FormatIntegerAsDecimal'), + ('access_time', 'access_time', '_FormatIntegerAsDecimal'), + ('allocated_file_size', 'allocated_file_size', '_FormatIntegerAsDecimal'), + ('file_size', 'file_size', '_FormatIntegerAsDecimal'), + ('file_attribute_flags', 'file_attribute_flags', + '_FormatIntegerAsDecimal'), + ('extended_data', 'extended_data', '_FormatIntegerAsDecimal'), + ('name_size', 'name_size', '_FormatIntegerAsDecimal'), + ('name_space', 'name_space', '_FormatIntegerAsDecimal'), + ('filename', 'filename', '_FormatString')] + + def PrintRecord(self, record): + """ + Prints a human readable version of the INDX record + to STDOUT. + + Args: + record (index_dir_entry): An index_dir_entry structure. + """ + if record is not None: + if self._debug: + self._DebugPrintStructureObject( + record.entry_header, self._DEBUG_INDX_ENTRY_HEADER) + self._DebugPrintStructureObject( + record.node_header, self._DEBUG_INDX_NODE_HEADER) + self._DebugPrintStructureObject( + record, self._DEBUG_INDX_DIR_RECORD) + self._DebugPrintStructureObject( + record.index_key_data, self._DEBUG_FILE_NAME_ATTR) + + + def _ParseIndexEntryHeader(self, file_object): + file_offset = file_object.tell() + data_type_map = self._GetDataTypeMap('index_entry_header') + + indx_record, data_size = self._ReadStructureFromFileObject( + file_object, file_offset, data_type_map, 'INDX Entry Header') + return indx_record, data_size + + + def _ParseIndexNodeHeader(self, file_object): + file_offset = file_object.tell() + data_type_map = self._GetDataTypeMap('index_node_header') + + indx_record, data_size = self._ReadStructureFromFileObject( + file_object, file_offset, data_type_map, 'INDX Node Header') + return indx_record, data_size + + + def _ParseIndexDirectoryEntry(self, file_object): + file_offset = file_object.tell() + data_type_map = self._GetDataTypeMap('index_dir_entry') + + try: + indx_record, data_size = self._ReadStructureFromFileObject( + file_object, file_offset, data_type_map, + '4KB block with possible INDX Directory Entry') + return indx_record, data_size + except ParseError as e: + if self._debug: + print(e) + return None, None + + + def ReadFileObject(self, file_object): + """Reads a file-like object containing INDX records. + + Args: + file_object (file): file-like object. + + Raises: + ParseError: if the file cannot be read. + """ + self._file_object = file_object + + def ReadRecords(self): + """ + Reads INDX records. + + Yields: + index_dir_entry: An $I30 INDX record. + + Raises: + ParseError: if a record cannot be read. + """ + self._file_object.seek(0, os.SEEK_SET) + file_offset = 0 + + # INDX entries allocated in 4096-byte chunks + block_size = 4096 + + while file_offset < self._file_size: + self._file_object.seek(file_offset, os.SEEK_SET) + index_dir_entry, _ = self._ParseIndexDirectoryEntry(self._file_object) + if index_dir_entry: + yield index_dir_entry + + file_offset += block_size diff --git a/dtformats/indx_directory_entry.yml b/dtformats/indx_directory_entry.yml new file mode 100644 index 0000000..e894811 --- /dev/null +++ b/dtformats/indx_directory_entry.yml @@ -0,0 +1,158 @@ +name: index_record +type: format +description: Index Directory Entry +urls: ["https://github.com/libyal/libfsntfs/blob/83c2f4ce3d16b5535eae9de767adc93fff724004/documentation/New%20Technologies%20File%20System%20(NTFS).asciidoc#index"] +metadata: + authors: ['Joachim Metz ', 'Juan Leaniz 64 + data_type: file_name_attribute diff --git a/test_data/indx.records b/test_data/indx.records new file mode 100644 index 0000000000000000000000000000000000000000..1d1240edf03c626d2e16dda8a030dc95ecb328f3 GIT binary patch literal 8192 zcmeI0U2IfE6vxlq4WVQ)V!{*AfVDtLQ&O6yHbf<*r5{2|g+_UetFJlnt^$iUOA7+pUKZP&c*V>NTUU z={}#``(-w0_&ss!mxtM4<_npQH-F0O)Psg3a-LD6(WTTx&Geq(^iUVBgnO;*I=I3U zDffj(aSasiRKs0JD`+LP())(rPpd5cyv;Yj7kS{s`~~|s4$(C=Ukkr1(0&Vl5iR5> z8tH@QY^RoEQFik2=)fgk;Z*s;fqG8MK{YuTxQ zKM~5%I$TLSdk*hK|Ll77s8aQd#3T2KN2o4Zik<1)|10?~POJIX$UogbagI%uxu^pk zK3W%hV|Qv`Z`SP#u;)rVW>+(mIwkrc4N*0ZW$pibvPvM9ApD{Up68vQ#S9$oS{%Qh$U;114?@ByoSB9UYEzEB%Z_;-)= zfdA>?o8Y_BMng0FJ{n+t3$^3s8V_H_y}Spm^z&ON&Dj~|QL0KE>TrHqp7c)I|3Ub# z=hyoGo!?K}ncv1SsM6r!A77t@|Gz!Hh5Wk`kJ**U{{ZKvnzOIxxy2s-lXIKEKX&#< z@Lg%6p&9;1cz@qE>)RX8;6I-IGx)Cb^EY$`>~;!v zdOrUZd{-LmcwzFCqF#=pljm+#8d3ZCr(<`)cO@RPs~lg=L!D;cZ}RYa+qS`fmn*uB zhGzH~%yV6auV>S+|7T7Pfj={K5B|IIX?!(KmzsWV_4uDjk4(T%kl1+^zstM_dNvCC zFZ@*Y0sMERpFfEAc$ImN^=jtlU#|TFd{_GUL$rhQvzljWRXR|I`Bz4FB0uGy{RzG+ z@t9qi{v9SY&&5st^(YPVE9!PnlE0JRS+$Sog!Okk{tI9BMXt16hsORVc%P#AZDt># zSIe;fvd*|%(I@7seURqIX|2^nB`?B1*=Id3zGbweI2o95|L3$Rsl}Ut1ht#<|GFAX zXSq<-`F}<ON7Kjhp%T;uWLsxDZ*pi_d31>9XXUHias9E=}{fPwz|T7@q7~ zgdhL=W$-UzeL*p#d3L=tbh94 zL%ICP_muydzp{wRUpJqB)VBxq6+#!ugKNhZzucGjMM@rAI}(pEhr_U1r;4-0iZiTI wR0U?B)c*x0sz{ra`oBm!w!pAT{a;|BinLj&|BJL^3k<8&{{<$hNSnpzA6M=d{r~^~ literal 0 HcmV?d00001 diff --git a/tests/indx_directory_entry.py b/tests/indx_directory_entry.py new file mode 100644 index 0000000..0eb735e --- /dev/null +++ b/tests/indx_directory_entry.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +"""Tests for NTFS INDX directory entries.""" + +import unittest +import os + +from dtformats import indx_directory_entry + +from tests import test_lib + + +class INDXRecordFileTest(test_lib.BaseTestCase): + """INDX record tests.""" + + expected_values = [ + {'entry_header_signature': b'INDX', + 'entry_header_fixup_value_offset': 40, + 'entry_header_num_fixup_values': 9, + 'entry_header_logfile_sequence_number': 1143279, + 'entry_header_virtual_cluster_number': 0, + 'node_header_index_values_offset': 40, + 'node_header_index_node_size': 3176, + 'node_header_allocated_index_node_size': 4072, + 'node_header_index_node_flags': 0, + 'file_reference' : 281474976710748, + 'index_value_size' : 88, + 'index_key_data_size' : 72, + 'index_value_flags' : 0, + 'index_key_data_parent_file_reference': 281474976710691, + 'index_key_data_creation_time': 130795153668593750, + 'index_key_data_modification_time': 130870367262995000, + 'index_key_data_entry_modification_time': 130849843578932500, + 'index_key_data_access_time': 130870367262995000, + 'index_key_data_allocated_file_size': 24576, + 'index_key_data_file_size': 24576, + 'index_key_data_file_attribute_flags': 38, + 'index_key_data_extended_data': 0, + 'index_key_data_name_size': 3, + 'index_key_data_name_space': 3, + 'index_key_data_filename': 'BCD', + }, + {'entry_header_signature': b'INDX', + 'entry_header_fixup_value_offset': 40, + 'entry_header_num_fixup_values': 9, + 'entry_header_logfile_sequence_number': 1060954, + 'entry_header_virtual_cluster_number': 0, + 'node_header_index_values_offset': 40, + 'node_header_index_node_size': 784, + 'node_header_allocated_index_node_size': 4072, + 'node_header_index_node_flags': 0, + 'file_reference' : 281474976710687, + 'index_value_size' : 96, + 'index_key_data_size' : 76, + 'index_value_flags' : 0, + 'index_key_data_parent_file_reference': 281474976710685, + 'index_key_data_creation_time': 130795138741093750, + 'index_key_data_modification_time': 130795138741093750, + 'index_key_data_entry_modification_time': 130795138741093750, + 'index_key_data_access_time': 130795138741093750, + 'index_key_data_allocated_file_size': 0, + 'index_key_data_file_size': 0, + 'index_key_data_file_attribute_flags': 38, + 'index_key_data_extended_data': 0, + 'index_key_data_name_size': 5, + 'index_key_data_name_space': 0, + 'index_key_data_filename': '$Tops', + }] + + + def _GetTestStructure(self, path): + test_file_path = self._GetTestFilePath([path]) + self._SkipIfPathNotExists(test_file_path) + + output_writer = test_lib.TestOutputWriter() + test_file = indx_directory_entry.INDXRecord( + debug=True, output_writer=output_writer) + + test_file.Open(test_file_path) + return test_file + + + def testReadFileObject(self): + """Tests the ReadFileObject function.""" + test_file = self._GetTestStructure('indx.records') + self.assertIsNotNone(test_file) + + + def testParseIndexEntryHeader(self): + """Tests the _ParseIndexEntryHeader function. """ + test_file = self._GetTestStructure('indx.records') + record, _ = test_file._ParseIndexEntryHeader(test_file._file_object) + + self.assertEqual(record.signature, + self.expected_values[0].get('entry_header_signature')) + self.assertEqual(record.fixup_value_offset, + self.expected_values[0].get('entry_header_fixup_value_offset')) + self.assertEqual(record.num_fixup_values, + self.expected_values[0].get('entry_header_num_fixup_values')) + self.assertEqual(record.logfile_sequence_number, + self.expected_values[0].get('entry_header_logfile_sequence_number')) + self.assertEqual(record.virtual_cluster_number, + self.expected_values[0].get('entry_header_virtual_cluster_number')) + + + def testParseIndexNodeHeader(self): + """Tests the _ParseIndexNodeHeader function. """ + test_file = self._GetTestStructure('indx.records') + test_file._file_object.seek(24, os.SEEK_SET) + record, _ = test_file._ParseIndexNodeHeader(test_file._file_object) + + self.assertEqual(record.index_values_offset, + self.expected_values[0].get('node_header_index_values_offset')) + self.assertEqual(record.index_node_size, + self.expected_values[0].get('node_header_index_node_size')) + self.assertEqual(record.allocated_index_node_size, + self.expected_values[0].get('node_header_allocated_index_node_size')) + self.assertEqual(record.index_node_flags, + self.expected_values[0].get('node_header_index_node_flags')) + + def testReadRecords(self): + """ + Method to test class functionality. + """ + test_file = self._GetTestStructure('indx.records') + + i = 0 + for r in test_file.ReadRecords(): + self.assertEqual(r.entry_header.signature, + self.expected_values[i].get('entry_header_signature')) + self.assertEqual(r.entry_header.fixup_value_offset, + self.expected_values[i].get('entry_header_fixup_value_offset')) + self.assertEqual(r.entry_header.num_fixup_values, + self.expected_values[i].get('entry_header_num_fixup_values')) + self.assertEqual(r.entry_header.logfile_sequence_number, + self.expected_values[i].get('entry_header_logfile_sequence_number')) + self.assertEqual(r.entry_header.virtual_cluster_number, + self.expected_values[i].get('entry_header_virtual_cluster_number')) + self.assertEqual(r.node_header.index_values_offset, + self.expected_values[i].get('node_header_index_values_offset')) + self.assertEqual(r.node_header.index_node_size, + self.expected_values[i].get('node_header_index_node_size')) + self.assertEqual(r.node_header.allocated_index_node_size, + self.expected_values[i].get('node_header_allocated_index_node_size')) + self.assertEqual(r.node_header.index_node_flags, + self.expected_values[i].get('node_header_index_node_flags')) + self.assertEqual(r.file_reference, + self.expected_values[i].get('file_reference')) + self.assertEqual(r.index_value_size, + self.expected_values[i].get('index_value_size')) + self.assertEqual(r.index_key_data_size, + self.expected_values[i].get('index_key_data_size')) + self.assertEqual(r.index_value_flags, + self.expected_values[i].get('index_value_flags')) + self.assertEqual(r.index_key_data.parent_file_reference, + self.expected_values[i].get('index_key_data_parent_file_reference')) + self.assertEqual(r.index_key_data.creation_time, + self.expected_values[i].get('index_key_data_creation_time')) + self.assertEqual(r.index_key_data.modification_time, + self.expected_values[i].get('index_key_data_modification_time')) + self.assertEqual(r.index_key_data.entry_modification_time, + self.expected_values[i].get('index_key_data_entry_modification_time')) + self.assertEqual(r.index_key_data.access_time, + self.expected_values[i].get('index_key_data_access_time')) + self.assertEqual(r.index_key_data.allocated_file_size, + self.expected_values[i].get('index_key_data_allocated_file_size')) + self.assertEqual(r.index_key_data.file_size, + self.expected_values[i].get('index_key_data_file_size')) + self.assertEqual(r.index_key_data.file_attribute_flags, + self.expected_values[i].get('index_key_data_file_attribute_flags')) + self.assertEqual(r.index_key_data.extended_data, + self.expected_values[i].get('index_key_data_extended_data')) + self.assertEqual(r.index_key_data.name_size, + self.expected_values[i].get('index_key_data_name_size')) + self.assertEqual(r.index_key_data.name_space, + self.expected_values[i].get('index_key_data_name_space')) + self.assertEqual(r.index_key_data.filename, + self.expected_values[i].get('index_key_data_filename')) + + i += 1 + + +if __name__ == '__main__': + unittest.main() From 5066fee841c3c293d8c24d2b553bcbc132c8c922 Mon Sep 17 00:00:00 2001 From: jleaniz Date: Mon, 22 Nov 2021 20:48:31 +0000 Subject: [PATCH 3/6] new formatting method and code style changes --- dtformats/data_format.py | 11 ++ ...tory_entry.py => index_directory_entry.py} | 121 ++++++++++-------- ...ry_entry.yml => index_directory_entry.yml} | 14 +- ...tory_entry.py => index_directory_entry.py} | 25 ++-- 4 files changed, 104 insertions(+), 67 deletions(-) rename dtformats/{indx_directory_entry.py => index_directory_entry.py} (51%) rename dtformats/{indx_directory_entry.yml => index_directory_entry.yml} (85%) rename tests/{indx_directory_entry.py => index_directory_entry.py} (94%) diff --git a/dtformats/data_format.py b/dtformats/data_format.py index 74c846a..e4953f1 100644 --- a/dtformats/data_format.py +++ b/dtformats/data_format.py @@ -394,6 +394,17 @@ def _FormatString(self, string): """ return string.rstrip('\x00') + def _FormatByteAsString(self, bytes_object): + """Formats a bytes object as a string. + + Args: + bytes_object (bytes): bytes object + + Returns: + str: a string decoded from bytes + """ + return bytes_object.decode('utf-8') + def _FormatStructureObject(self, structure_object, debug_info): """Formats a structure object debug information. diff --git a/dtformats/indx_directory_entry.py b/dtformats/index_directory_entry.py similarity index 51% rename from dtformats/indx_directory_entry.py rename to dtformats/index_directory_entry.py index 955089f..050b968 100644 --- a/dtformats/indx_directory_entry.py +++ b/dtformats/index_directory_entry.py @@ -1,36 +1,33 @@ # -*- coding: utf-8 -*- -"""INDX entries """ +""" NTFS $I30 index entries """ import os from dtformats import data_format -from dtformats.errors import ParseError +from dtformats import errors -class INDXRecord(data_format.BinaryDataFile): - """ Class that represents an INDX record. - - Note: currently only supports $I30 index records - """ +class NTFSIndexI30Record(data_format.BinaryDataFile): + """Class that represents an NTFS $I30 index record.""" _FABRIC = data_format.BinaryDataFile.ReadDefinitionFile( - 'indx_directory_entry.yml') + 'index_directory_entry.yml') _DEBUG_INDX_ENTRY_HEADER = [ - ('signature', 'signature', ''), - ('fixup_value_offset', 'fixup_value_offset', '_FormatIntegerAsDecimal'), - ('num_fixup_values', 'num_fixup_values', '_FormatIntegerAsDecimal'), - ('logfile_sequence_number', 'logfile_sequence_number', - '_FormatIntegerAsDecimal'), - ('virtual_cluster_number', 'virtual_cluster_number', - '_FormatIntegerAsDecimal')] + ('signature', 'signature', '_FormatByteAsString'), + ('fixup_value_offset', 'fixup_value_offset', '_FormatIntegerAsDecimal'), + ('num_fixup_values', 'num_fixup_values', '_FormatIntegerAsDecimal'), + ('logfile_sequence_number', 'logfile_sequence_number', + '_FormatIntegerAsDecimal'), + ('virtual_cluster_number', 'virtual_cluster_number', + '_FormatIntegerAsDecimal')] _DEBUG_INDX_NODE_HEADER = [ - ('index_values_offset', 'index_values_offset', - '_FormatIntegerAsDecimal'), - ('index_node_size', 'index_node_size', '_FormatIntegerAsDecimal'), - ('allocated_index_node_size', 'allocated_index_node_size', - '_FormatIntegerAsDecimal'), - ('index_node_flags', 'index_node_flags', '_FormatIntegerAsDecimal')] + ('index_values_offset', 'index_values_offset', + '_FormatIntegerAsDecimal'), + ('index_node_size', 'index_node_size', '_FormatIntegerAsDecimal'), + ('allocated_index_node_size', 'allocated_index_node_size', + '_FormatIntegerAsDecimal'), + ('index_node_flags', 'index_node_flags', '_FormatIntegerAsDecimal')] _DEBUG_INDX_DIR_RECORD = [ ('file_reference', 'file_reference', '_FormatIntegerAsDecimal'), @@ -56,58 +53,81 @@ class INDXRecord(data_format.BinaryDataFile): ('filename', 'filename', '_FormatString')] def PrintRecord(self, record): - """ - Prints a human readable version of the INDX record - to STDOUT. + """Prints a human readable version of the NTFS $I30 + index record. Args: - record (index_dir_entry): An index_dir_entry structure. + record (index_directory_entry): An index_directory_entry structure. """ - if record is not None: + if record: if self._debug: self._DebugPrintStructureObject( - record.entry_header, self._DEBUG_INDX_ENTRY_HEADER) + record.entry_header, self._DEBUG_INDX_ENTRY_HEADER) self._DebugPrintStructureObject( - record.node_header, self._DEBUG_INDX_NODE_HEADER) + record.node_header, self._DEBUG_INDX_NODE_HEADER) self._DebugPrintStructureObject( - record, self._DEBUG_INDX_DIR_RECORD) + record, self._DEBUG_INDX_DIR_RECORD) self._DebugPrintStructureObject( - record.index_key_data, self._DEBUG_FILE_NAME_ATTR) - + record.index_key_data, self._DEBUG_FILE_NAME_ATTR) def _ParseIndexEntryHeader(self, file_object): + """Parses an NTFS index entry header. + + Args: + file_object: A file-like object. + + Returns: + tuple[index_entry_header, integer]: A tuple containing an + index entry header structure and data size. + """ file_offset = file_object.tell() data_type_map = self._GetDataTypeMap('index_entry_header') - indx_record, data_size = self._ReadStructureFromFileObject( + index_entry_header, data_size = self._ReadStructureFromFileObject( file_object, file_offset, data_type_map, 'INDX Entry Header') - return indx_record, data_size - + return index_entry_header, data_size def _ParseIndexNodeHeader(self, file_object): + """Parses an NTFS index node header. + + Args: + file_object: A file-like object. + + Returns: + tuple[index_node_header, integer]: A tuple containing an index node + header structure and data size. + """ file_offset = file_object.tell() data_type_map = self._GetDataTypeMap('index_node_header') - indx_record, data_size = self._ReadStructureFromFileObject( + index_node_header, data_size = self._ReadStructureFromFileObject( file_object, file_offset, data_type_map, 'INDX Node Header') - return indx_record, data_size - + return index_node_header, data_size def _ParseIndexDirectoryEntry(self, file_object): + """Parses an NTFS $I30 index record that contains + directory entries. + + Args: + file_object: A file-like object. + + Returns: + tuple[index_directory_entry, integer]: A tuple containing a + NTFS $I30 index structure and data size. + """ file_offset = file_object.tell() - data_type_map = self._GetDataTypeMap('index_dir_entry') + data_type_map = self._GetDataTypeMap('index_directory_entry') try: - indx_record, data_size = self._ReadStructureFromFileObject( + index_directory_entry, data_size = self._ReadStructureFromFileObject( file_object, file_offset, data_type_map, '4KB block with possible INDX Directory Entry') - return indx_record, data_size - except ParseError as e: + return index_directory_entry, data_size + except errors.ParseError as exception: if self._debug: - print(e) + print(exception) return None, None - def ReadFileObject(self, file_object): """Reads a file-like object containing INDX records. @@ -119,12 +139,12 @@ def ReadFileObject(self, file_object): """ self._file_object = file_object + def ReadRecords(self): - """ - Reads INDX records. + """Reads INDX records. Yields: - index_dir_entry: An $I30 INDX record. + index_directory_entry: An NTFS $I30 index record. Raises: ParseError: if a record cannot be read. @@ -132,13 +152,14 @@ def ReadRecords(self): self._file_object.seek(0, os.SEEK_SET) file_offset = 0 - # INDX entries allocated in 4096-byte chunks + # NTFS INDX entries allocated in 4096-byte chunks block_size = 4096 while file_offset < self._file_size: self._file_object.seek(file_offset, os.SEEK_SET) - index_dir_entry, _ = self._ParseIndexDirectoryEntry(self._file_object) - if index_dir_entry: - yield index_dir_entry + index_directory_entry, _ = self._ParseIndexDirectoryEntry( + self._file_object) + if index_directory_entry: + yield index_directory_entry file_offset += block_size diff --git a/dtformats/indx_directory_entry.yml b/dtformats/index_directory_entry.yml similarity index 85% rename from dtformats/indx_directory_entry.yml rename to dtformats/index_directory_entry.yml index e894811..0ee319a 100644 --- a/dtformats/indx_directory_entry.yml +++ b/dtformats/index_directory_entry.yml @@ -1,9 +1,9 @@ -name: index_record +name: ntfs_i30_index type: format -description: Index Directory Entry -urls: ["https://github.com/libyal/libfsntfs/blob/83c2f4ce3d16b5535eae9de767adc93fff724004/documentation/New%20Technologies%20File%20System%20(NTFS).asciidoc#index"] +description: NTFS $I30 index record that contains directory entries +urls: ["https://github.com/libyal/libfsntfs/blob/main/documentation/New%20Technologies%20File%20System%20(NTFS).asciidoc#index"] metadata: - authors: ['Joachim Metz ', 'Juan Leaniz 64 + condition: index_directory_entry.index_key_data_size > 64 data_type: file_name_attribute diff --git a/tests/indx_directory_entry.py b/tests/index_directory_entry.py similarity index 94% rename from tests/indx_directory_entry.py rename to tests/index_directory_entry.py index 0eb735e..2e2e3ea 100644 --- a/tests/indx_directory_entry.py +++ b/tests/index_directory_entry.py @@ -4,13 +4,13 @@ import unittest import os -from dtformats import indx_directory_entry +from dtformats import index_directory_entry from tests import test_lib - -class INDXRecordFileTest(test_lib.BaseTestCase): - """INDX record tests.""" + # pylint: disable=protected-access +class IndexRecordFileTest(test_lib.BaseTestCase): + """NTFS $I30 index record tests.""" expected_values = [ {'entry_header_signature': b'INDX', @@ -66,25 +66,31 @@ class INDXRecordFileTest(test_lib.BaseTestCase): 'index_key_data_filename': '$Tops', }] - def _GetTestStructure(self, path): - test_file_path = self._GetTestFilePath([path]) + """ Helper method to create an NTFSIndexI30Record + object. + + Args: + path (str): Path to a test file. + + Returns: + NTFSIndexI30Record: An NTFS I30 index object. + """ + test_file_path = self._GetTestFilePath(path) self._SkipIfPathNotExists(test_file_path) output_writer = test_lib.TestOutputWriter() - test_file = indx_directory_entry.INDXRecord( + test_file = index_directory_entry.NTFSIndexI30Record( debug=True, output_writer=output_writer) test_file.Open(test_file_path) return test_file - def testReadFileObject(self): """Tests the ReadFileObject function.""" test_file = self._GetTestStructure('indx.records') self.assertIsNotNone(test_file) - def testParseIndexEntryHeader(self): """Tests the _ParseIndexEntryHeader function. """ test_file = self._GetTestStructure('indx.records') @@ -101,7 +107,6 @@ def testParseIndexEntryHeader(self): self.assertEqual(record.virtual_cluster_number, self.expected_values[0].get('entry_header_virtual_cluster_number')) - def testParseIndexNodeHeader(self): """Tests the _ParseIndexNodeHeader function. """ test_file = self._GetTestStructure('indx.records') From 40a44a5e1d4f910621d3b97c3445caf04c1fca59 Mon Sep 17 00:00:00 2001 From: jleaniz Date: Mon, 22 Nov 2021 21:01:47 +0000 Subject: [PATCH 4/6] remove old files --- dtformats/indx_directory_entry.py | 144 ----------------------- dtformats/indx_directory_entry.yml | 158 ------------------------- tests/indx_directory_entry.py | 183 ----------------------------- 3 files changed, 485 deletions(-) delete mode 100644 dtformats/indx_directory_entry.py delete mode 100644 dtformats/indx_directory_entry.yml delete mode 100644 tests/indx_directory_entry.py diff --git a/dtformats/indx_directory_entry.py b/dtformats/indx_directory_entry.py deleted file mode 100644 index 955089f..0000000 --- a/dtformats/indx_directory_entry.py +++ /dev/null @@ -1,144 +0,0 @@ -# -*- coding: utf-8 -*- -"""INDX entries """ - -import os - -from dtformats import data_format -from dtformats.errors import ParseError - -class INDXRecord(data_format.BinaryDataFile): - """ Class that represents an INDX record. - - Note: currently only supports $I30 index records - """ - - _FABRIC = data_format.BinaryDataFile.ReadDefinitionFile( - 'indx_directory_entry.yml') - - _DEBUG_INDX_ENTRY_HEADER = [ - ('signature', 'signature', ''), - ('fixup_value_offset', 'fixup_value_offset', '_FormatIntegerAsDecimal'), - ('num_fixup_values', 'num_fixup_values', '_FormatIntegerAsDecimal'), - ('logfile_sequence_number', 'logfile_sequence_number', - '_FormatIntegerAsDecimal'), - ('virtual_cluster_number', 'virtual_cluster_number', - '_FormatIntegerAsDecimal')] - - _DEBUG_INDX_NODE_HEADER = [ - ('index_values_offset', 'index_values_offset', - '_FormatIntegerAsDecimal'), - ('index_node_size', 'index_node_size', '_FormatIntegerAsDecimal'), - ('allocated_index_node_size', 'allocated_index_node_size', - '_FormatIntegerAsDecimal'), - ('index_node_flags', 'index_node_flags', '_FormatIntegerAsDecimal')] - - _DEBUG_INDX_DIR_RECORD = [ - ('file_reference', 'file_reference', '_FormatIntegerAsDecimal'), - ('index_value_size', 'index_value_size', '_FormatIntegerAsDecimal'), - ('index_key_data_size', 'index_key_data_size', '_FormatIntegerAsDecimal'), - ('index_value_flags', 'index_value_flags', '_FormatIntegerAsDecimal')] - - _DEBUG_FILE_NAME_ATTR = [ - ('parent_file_reference', 'parent_file_reference', - '_FormatIntegerAsDecimal'), - ('creation_time', 'creation_time', '_FormatIntegerAsDecimal'), - ('modification_time', 'modification_time', '_FormatIntegerAsDecimal'), - ('entry_modification_time', 'entry_modification_time', - '_FormatIntegerAsDecimal'), - ('access_time', 'access_time', '_FormatIntegerAsDecimal'), - ('allocated_file_size', 'allocated_file_size', '_FormatIntegerAsDecimal'), - ('file_size', 'file_size', '_FormatIntegerAsDecimal'), - ('file_attribute_flags', 'file_attribute_flags', - '_FormatIntegerAsDecimal'), - ('extended_data', 'extended_data', '_FormatIntegerAsDecimal'), - ('name_size', 'name_size', '_FormatIntegerAsDecimal'), - ('name_space', 'name_space', '_FormatIntegerAsDecimal'), - ('filename', 'filename', '_FormatString')] - - def PrintRecord(self, record): - """ - Prints a human readable version of the INDX record - to STDOUT. - - Args: - record (index_dir_entry): An index_dir_entry structure. - """ - if record is not None: - if self._debug: - self._DebugPrintStructureObject( - record.entry_header, self._DEBUG_INDX_ENTRY_HEADER) - self._DebugPrintStructureObject( - record.node_header, self._DEBUG_INDX_NODE_HEADER) - self._DebugPrintStructureObject( - record, self._DEBUG_INDX_DIR_RECORD) - self._DebugPrintStructureObject( - record.index_key_data, self._DEBUG_FILE_NAME_ATTR) - - - def _ParseIndexEntryHeader(self, file_object): - file_offset = file_object.tell() - data_type_map = self._GetDataTypeMap('index_entry_header') - - indx_record, data_size = self._ReadStructureFromFileObject( - file_object, file_offset, data_type_map, 'INDX Entry Header') - return indx_record, data_size - - - def _ParseIndexNodeHeader(self, file_object): - file_offset = file_object.tell() - data_type_map = self._GetDataTypeMap('index_node_header') - - indx_record, data_size = self._ReadStructureFromFileObject( - file_object, file_offset, data_type_map, 'INDX Node Header') - return indx_record, data_size - - - def _ParseIndexDirectoryEntry(self, file_object): - file_offset = file_object.tell() - data_type_map = self._GetDataTypeMap('index_dir_entry') - - try: - indx_record, data_size = self._ReadStructureFromFileObject( - file_object, file_offset, data_type_map, - '4KB block with possible INDX Directory Entry') - return indx_record, data_size - except ParseError as e: - if self._debug: - print(e) - return None, None - - - def ReadFileObject(self, file_object): - """Reads a file-like object containing INDX records. - - Args: - file_object (file): file-like object. - - Raises: - ParseError: if the file cannot be read. - """ - self._file_object = file_object - - def ReadRecords(self): - """ - Reads INDX records. - - Yields: - index_dir_entry: An $I30 INDX record. - - Raises: - ParseError: if a record cannot be read. - """ - self._file_object.seek(0, os.SEEK_SET) - file_offset = 0 - - # INDX entries allocated in 4096-byte chunks - block_size = 4096 - - while file_offset < self._file_size: - self._file_object.seek(file_offset, os.SEEK_SET) - index_dir_entry, _ = self._ParseIndexDirectoryEntry(self._file_object) - if index_dir_entry: - yield index_dir_entry - - file_offset += block_size diff --git a/dtformats/indx_directory_entry.yml b/dtformats/indx_directory_entry.yml deleted file mode 100644 index e894811..0000000 --- a/dtformats/indx_directory_entry.yml +++ /dev/null @@ -1,158 +0,0 @@ -name: index_record -type: format -description: Index Directory Entry -urls: ["https://github.com/libyal/libfsntfs/blob/83c2f4ce3d16b5535eae9de767adc93fff724004/documentation/New%20Technologies%20File%20System%20(NTFS).asciidoc#index"] -metadata: - authors: ['Joachim Metz ', 'Juan Leaniz 64 - data_type: file_name_attribute diff --git a/tests/indx_directory_entry.py b/tests/indx_directory_entry.py deleted file mode 100644 index 0eb735e..0000000 --- a/tests/indx_directory_entry.py +++ /dev/null @@ -1,183 +0,0 @@ -# -*- coding: utf-8 -*- -"""Tests for NTFS INDX directory entries.""" - -import unittest -import os - -from dtformats import indx_directory_entry - -from tests import test_lib - - -class INDXRecordFileTest(test_lib.BaseTestCase): - """INDX record tests.""" - - expected_values = [ - {'entry_header_signature': b'INDX', - 'entry_header_fixup_value_offset': 40, - 'entry_header_num_fixup_values': 9, - 'entry_header_logfile_sequence_number': 1143279, - 'entry_header_virtual_cluster_number': 0, - 'node_header_index_values_offset': 40, - 'node_header_index_node_size': 3176, - 'node_header_allocated_index_node_size': 4072, - 'node_header_index_node_flags': 0, - 'file_reference' : 281474976710748, - 'index_value_size' : 88, - 'index_key_data_size' : 72, - 'index_value_flags' : 0, - 'index_key_data_parent_file_reference': 281474976710691, - 'index_key_data_creation_time': 130795153668593750, - 'index_key_data_modification_time': 130870367262995000, - 'index_key_data_entry_modification_time': 130849843578932500, - 'index_key_data_access_time': 130870367262995000, - 'index_key_data_allocated_file_size': 24576, - 'index_key_data_file_size': 24576, - 'index_key_data_file_attribute_flags': 38, - 'index_key_data_extended_data': 0, - 'index_key_data_name_size': 3, - 'index_key_data_name_space': 3, - 'index_key_data_filename': 'BCD', - }, - {'entry_header_signature': b'INDX', - 'entry_header_fixup_value_offset': 40, - 'entry_header_num_fixup_values': 9, - 'entry_header_logfile_sequence_number': 1060954, - 'entry_header_virtual_cluster_number': 0, - 'node_header_index_values_offset': 40, - 'node_header_index_node_size': 784, - 'node_header_allocated_index_node_size': 4072, - 'node_header_index_node_flags': 0, - 'file_reference' : 281474976710687, - 'index_value_size' : 96, - 'index_key_data_size' : 76, - 'index_value_flags' : 0, - 'index_key_data_parent_file_reference': 281474976710685, - 'index_key_data_creation_time': 130795138741093750, - 'index_key_data_modification_time': 130795138741093750, - 'index_key_data_entry_modification_time': 130795138741093750, - 'index_key_data_access_time': 130795138741093750, - 'index_key_data_allocated_file_size': 0, - 'index_key_data_file_size': 0, - 'index_key_data_file_attribute_flags': 38, - 'index_key_data_extended_data': 0, - 'index_key_data_name_size': 5, - 'index_key_data_name_space': 0, - 'index_key_data_filename': '$Tops', - }] - - - def _GetTestStructure(self, path): - test_file_path = self._GetTestFilePath([path]) - self._SkipIfPathNotExists(test_file_path) - - output_writer = test_lib.TestOutputWriter() - test_file = indx_directory_entry.INDXRecord( - debug=True, output_writer=output_writer) - - test_file.Open(test_file_path) - return test_file - - - def testReadFileObject(self): - """Tests the ReadFileObject function.""" - test_file = self._GetTestStructure('indx.records') - self.assertIsNotNone(test_file) - - - def testParseIndexEntryHeader(self): - """Tests the _ParseIndexEntryHeader function. """ - test_file = self._GetTestStructure('indx.records') - record, _ = test_file._ParseIndexEntryHeader(test_file._file_object) - - self.assertEqual(record.signature, - self.expected_values[0].get('entry_header_signature')) - self.assertEqual(record.fixup_value_offset, - self.expected_values[0].get('entry_header_fixup_value_offset')) - self.assertEqual(record.num_fixup_values, - self.expected_values[0].get('entry_header_num_fixup_values')) - self.assertEqual(record.logfile_sequence_number, - self.expected_values[0].get('entry_header_logfile_sequence_number')) - self.assertEqual(record.virtual_cluster_number, - self.expected_values[0].get('entry_header_virtual_cluster_number')) - - - def testParseIndexNodeHeader(self): - """Tests the _ParseIndexNodeHeader function. """ - test_file = self._GetTestStructure('indx.records') - test_file._file_object.seek(24, os.SEEK_SET) - record, _ = test_file._ParseIndexNodeHeader(test_file._file_object) - - self.assertEqual(record.index_values_offset, - self.expected_values[0].get('node_header_index_values_offset')) - self.assertEqual(record.index_node_size, - self.expected_values[0].get('node_header_index_node_size')) - self.assertEqual(record.allocated_index_node_size, - self.expected_values[0].get('node_header_allocated_index_node_size')) - self.assertEqual(record.index_node_flags, - self.expected_values[0].get('node_header_index_node_flags')) - - def testReadRecords(self): - """ - Method to test class functionality. - """ - test_file = self._GetTestStructure('indx.records') - - i = 0 - for r in test_file.ReadRecords(): - self.assertEqual(r.entry_header.signature, - self.expected_values[i].get('entry_header_signature')) - self.assertEqual(r.entry_header.fixup_value_offset, - self.expected_values[i].get('entry_header_fixup_value_offset')) - self.assertEqual(r.entry_header.num_fixup_values, - self.expected_values[i].get('entry_header_num_fixup_values')) - self.assertEqual(r.entry_header.logfile_sequence_number, - self.expected_values[i].get('entry_header_logfile_sequence_number')) - self.assertEqual(r.entry_header.virtual_cluster_number, - self.expected_values[i].get('entry_header_virtual_cluster_number')) - self.assertEqual(r.node_header.index_values_offset, - self.expected_values[i].get('node_header_index_values_offset')) - self.assertEqual(r.node_header.index_node_size, - self.expected_values[i].get('node_header_index_node_size')) - self.assertEqual(r.node_header.allocated_index_node_size, - self.expected_values[i].get('node_header_allocated_index_node_size')) - self.assertEqual(r.node_header.index_node_flags, - self.expected_values[i].get('node_header_index_node_flags')) - self.assertEqual(r.file_reference, - self.expected_values[i].get('file_reference')) - self.assertEqual(r.index_value_size, - self.expected_values[i].get('index_value_size')) - self.assertEqual(r.index_key_data_size, - self.expected_values[i].get('index_key_data_size')) - self.assertEqual(r.index_value_flags, - self.expected_values[i].get('index_value_flags')) - self.assertEqual(r.index_key_data.parent_file_reference, - self.expected_values[i].get('index_key_data_parent_file_reference')) - self.assertEqual(r.index_key_data.creation_time, - self.expected_values[i].get('index_key_data_creation_time')) - self.assertEqual(r.index_key_data.modification_time, - self.expected_values[i].get('index_key_data_modification_time')) - self.assertEqual(r.index_key_data.entry_modification_time, - self.expected_values[i].get('index_key_data_entry_modification_time')) - self.assertEqual(r.index_key_data.access_time, - self.expected_values[i].get('index_key_data_access_time')) - self.assertEqual(r.index_key_data.allocated_file_size, - self.expected_values[i].get('index_key_data_allocated_file_size')) - self.assertEqual(r.index_key_data.file_size, - self.expected_values[i].get('index_key_data_file_size')) - self.assertEqual(r.index_key_data.file_attribute_flags, - self.expected_values[i].get('index_key_data_file_attribute_flags')) - self.assertEqual(r.index_key_data.extended_data, - self.expected_values[i].get('index_key_data_extended_data')) - self.assertEqual(r.index_key_data.name_size, - self.expected_values[i].get('index_key_data_name_size')) - self.assertEqual(r.index_key_data.name_space, - self.expected_values[i].get('index_key_data_name_space')) - self.assertEqual(r.index_key_data.filename, - self.expected_values[i].get('index_key_data_filename')) - - i += 1 - - -if __name__ == '__main__': - unittest.main() From 0f93c9a6ea49c2b112c0293ebb76fcdc6430d540 Mon Sep 17 00:00:00 2001 From: jleaniz Date: Mon, 6 Dec 2021 14:09:54 +0000 Subject: [PATCH 5/6] updated tests and fixed style issues --- dtformats/index_directory_entry.py | 2 +- test_data/indx.records | Bin 8192 -> 0 bytes tests/index_directory_entry.py | 115 +++++++++++++++-------------- 3 files changed, 59 insertions(+), 58 deletions(-) delete mode 100644 test_data/indx.records diff --git a/dtformats/index_directory_entry.py b/dtformats/index_directory_entry.py index 050b968..ffaf336 100644 --- a/dtformats/index_directory_entry.py +++ b/dtformats/index_directory_entry.py @@ -141,7 +141,7 @@ def ReadFileObject(self, file_object): def ReadRecords(self): - """Reads INDX records. + """Reads NTFS $I30 INDX records. Yields: index_directory_entry: An NTFS $I30 index record. diff --git a/test_data/indx.records b/test_data/indx.records deleted file mode 100644 index 1d1240edf03c626d2e16dda8a030dc95ecb328f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8192 zcmeI0U2IfE6vxlq4WVQ)V!{*AfVDtLQ&O6yHbf<*r5{2|g+_UetFJlnt^$iUOA7+pUKZP&c*V>NTUU z={}#``(-w0_&ss!mxtM4<_npQH-F0O)Psg3a-LD6(WTTx&Geq(^iUVBgnO;*I=I3U zDffj(aSasiRKs0JD`+LP())(rPpd5cyv;Yj7kS{s`~~|s4$(C=Ukkr1(0&Vl5iR5> z8tH@QY^RoEQFik2=)fgk;Z*s;fqG8MK{YuTxQ zKM~5%I$TLSdk*hK|Ll77s8aQd#3T2KN2o4Zik<1)|10?~POJIX$UogbagI%uxu^pk zK3W%hV|Qv`Z`SP#u;)rVW>+(mIwkrc4N*0ZW$pibvPvM9ApD{Up68vQ#S9$oS{%Qh$U;114?@ByoSB9UYEzEB%Z_;-)= zfdA>?o8Y_BMng0FJ{n+t3$^3s8V_H_y}Spm^z&ON&Dj~|QL0KE>TrHqp7c)I|3Ub# z=hyoGo!?K}ncv1SsM6r!A77t@|Gz!Hh5Wk`kJ**U{{ZKvnzOIxxy2s-lXIKEKX&#< z@Lg%6p&9;1cz@qE>)RX8;6I-IGx)Cb^EY$`>~;!v zdOrUZd{-LmcwzFCqF#=pljm+#8d3ZCr(<`)cO@RPs~lg=L!D;cZ}RYa+qS`fmn*uB zhGzH~%yV6auV>S+|7T7Pfj={K5B|IIX?!(KmzsWV_4uDjk4(T%kl1+^zstM_dNvCC zFZ@*Y0sMERpFfEAc$ImN^=jtlU#|TFd{_GUL$rhQvzljWRXR|I`Bz4FB0uGy{RzG+ z@t9qi{v9SY&&5st^(YPVE9!PnlE0JRS+$Sog!Okk{tI9BMXt16hsORVc%P#AZDt># zSIe;fvd*|%(I@7seURqIX|2^nB`?B1*=Id3zGbweI2o95|L3$Rsl}Ut1ht#<|GFAX zXSq<-`F}<ON7Kjhp%T;uWLsxDZ*pi_d31>9XXUHias9E=}{fPwz|T7@q7~ zgdhL=W$-UzeL*p#d3L=tbh94 zL%ICP_muydzp{wRUpJqB)VBxq6+#!ugKNhZzucGjMM@rAI}(pEhr_U1r;4-0iZiTI wR0U?B)c*x0sz{ra`oBm!w!pAT{a;|BinLj&|BJL^3k<8&{{<$hNSnpzA6M=d{r~^~ diff --git a/tests/index_directory_entry.py b/tests/index_directory_entry.py index 2e2e3ea..8de48c6 100644 --- a/tests/index_directory_entry.py +++ b/tests/index_directory_entry.py @@ -1,70 +1,71 @@ # -*- coding: utf-8 -*- """Tests for NTFS INDX directory entries.""" -import unittest import os +import unittest from dtformats import index_directory_entry from tests import test_lib + # pylint: disable=protected-access class IndexRecordFileTest(test_lib.BaseTestCase): """NTFS $I30 index record tests.""" expected_values = [ - {'entry_header_signature': b'INDX', - 'entry_header_fixup_value_offset': 40, - 'entry_header_num_fixup_values': 9, - 'entry_header_logfile_sequence_number': 1143279, - 'entry_header_virtual_cluster_number': 0, - 'node_header_index_values_offset': 40, - 'node_header_index_node_size': 3176, - 'node_header_allocated_index_node_size': 4072, - 'node_header_index_node_flags': 0, - 'file_reference' : 281474976710748, - 'index_value_size' : 88, - 'index_key_data_size' : 72, - 'index_value_flags' : 0, - 'index_key_data_parent_file_reference': 281474976710691, - 'index_key_data_creation_time': 130795153668593750, - 'index_key_data_modification_time': 130870367262995000, - 'index_key_data_entry_modification_time': 130849843578932500, - 'index_key_data_access_time': 130870367262995000, - 'index_key_data_allocated_file_size': 24576, - 'index_key_data_file_size': 24576, - 'index_key_data_file_attribute_flags': 38, - 'index_key_data_extended_data': 0, - 'index_key_data_name_size': 3, - 'index_key_data_name_space': 3, - 'index_key_data_filename': 'BCD', - }, - {'entry_header_signature': b'INDX', - 'entry_header_fixup_value_offset': 40, - 'entry_header_num_fixup_values': 9, - 'entry_header_logfile_sequence_number': 1060954, - 'entry_header_virtual_cluster_number': 0, - 'node_header_index_values_offset': 40, - 'node_header_index_node_size': 784, - 'node_header_allocated_index_node_size': 4072, - 'node_header_index_node_flags': 0, - 'file_reference' : 281474976710687, - 'index_value_size' : 96, - 'index_key_data_size' : 76, - 'index_value_flags' : 0, - 'index_key_data_parent_file_reference': 281474976710685, - 'index_key_data_creation_time': 130795138741093750, - 'index_key_data_modification_time': 130795138741093750, - 'index_key_data_entry_modification_time': 130795138741093750, - 'index_key_data_access_time': 130795138741093750, - 'index_key_data_allocated_file_size': 0, - 'index_key_data_file_size': 0, - 'index_key_data_file_attribute_flags': 38, - 'index_key_data_extended_data': 0, - 'index_key_data_name_size': 5, - 'index_key_data_name_space': 0, - 'index_key_data_filename': '$Tops', - }] + {'entry_header_signature': b'INDX', + 'entry_header_fixup_value_offset': 40, + 'entry_header_num_fixup_values': 9, + 'entry_header_logfile_sequence_number': 1143279, + 'entry_header_virtual_cluster_number': 0, + 'node_header_index_values_offset': 40, + 'node_header_index_node_size': 3176, + 'node_header_allocated_index_node_size': 4072, + 'node_header_index_node_flags': 0, + 'file_reference' : 281474976710748, + 'index_value_size' : 88, + 'index_key_data_size' : 72, + 'index_value_flags' : 0, + 'index_key_data_parent_file_reference': 281474976710691, + 'index_key_data_creation_time': 130795153668593750, + 'index_key_data_modification_time': 130870367262995000, + 'index_key_data_entry_modification_time': 130849843578932500, + 'index_key_data_access_time': 130870367262995000, + 'index_key_data_allocated_file_size': 24576, + 'index_key_data_file_size': 24576, + 'index_key_data_file_attribute_flags': 38, + 'index_key_data_extended_data': 0, + 'index_key_data_name_size': 3, + 'index_key_data_name_space': 3, + 'index_key_data_filename': 'BCD', + }, + {'entry_header_signature': b'INDX', + 'entry_header_fixup_value_offset': 40, + 'entry_header_num_fixup_values': 9, + 'entry_header_logfile_sequence_number': 1060954, + 'entry_header_virtual_cluster_number': 0, + 'node_header_index_values_offset': 40, + 'node_header_index_node_size': 784, + 'node_header_allocated_index_node_size': 4072, + 'node_header_index_node_flags': 0, + 'file_reference' : 281474976710687, + 'index_value_size' : 96, + 'index_key_data_size' : 76, + 'index_value_flags' : 0, + 'index_key_data_parent_file_reference': 281474976710685, + 'index_key_data_creation_time': 130795138741093750, + 'index_key_data_modification_time': 130795138741093750, + 'index_key_data_entry_modification_time': 130795138741093750, + 'index_key_data_access_time': 130795138741093750, + 'index_key_data_allocated_file_size': 0, + 'index_key_data_file_size': 0, + 'index_key_data_file_attribute_flags': 38, + 'index_key_data_extended_data': 0, + 'index_key_data_name_size': 5, + 'index_key_data_name_space': 0, + 'index_key_data_filename': '$Tops', + }] def _GetTestStructure(self, path): """ Helper method to create an NTFSIndexI30Record @@ -88,12 +89,12 @@ def _GetTestStructure(self, path): def testReadFileObject(self): """Tests the ReadFileObject function.""" - test_file = self._GetTestStructure('indx.records') + test_file = self._GetTestStructure(['ntfs_i30_records.raw']) self.assertIsNotNone(test_file) def testParseIndexEntryHeader(self): """Tests the _ParseIndexEntryHeader function. """ - test_file = self._GetTestStructure('indx.records') + test_file = self._GetTestStructure(['ntfs_i30_records.raw']) record, _ = test_file._ParseIndexEntryHeader(test_file._file_object) self.assertEqual(record.signature, @@ -109,7 +110,7 @@ def testParseIndexEntryHeader(self): def testParseIndexNodeHeader(self): """Tests the _ParseIndexNodeHeader function. """ - test_file = self._GetTestStructure('indx.records') + test_file = self._GetTestStructure(['ntfs_i30_records.raw']) test_file._file_object.seek(24, os.SEEK_SET) record, _ = test_file._ParseIndexNodeHeader(test_file._file_object) @@ -126,7 +127,7 @@ def testReadRecords(self): """ Method to test class functionality. """ - test_file = self._GetTestStructure('indx.records') + test_file = self._GetTestStructure(['ntfs_i30_records.raw']) i = 0 for r in test_file.ReadRecords(): From 7a8b5f815161091ad856edaa81cb1f3c417f1245 Mon Sep 17 00:00:00 2001 From: jleaniz Date: Mon, 6 Dec 2021 14:16:57 +0000 Subject: [PATCH 6/6] fix formatting --- dtformats/index_directory_entry.py | 17 ++++++++--------- tests/index_directory_entry.py | 3 +-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/dtformats/index_directory_entry.py b/dtformats/index_directory_entry.py index ffaf336..70882ba 100644 --- a/dtformats/index_directory_entry.py +++ b/dtformats/index_directory_entry.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- -""" NTFS $I30 index entries """ +"""NTFS $I30 index entries.""" import os from dtformats import data_format from dtformats import errors + class NTFSIndexI30Record(data_format.BinaryDataFile): """Class that represents an NTFS $I30 index record.""" @@ -17,16 +18,16 @@ class NTFSIndexI30Record(data_format.BinaryDataFile): ('fixup_value_offset', 'fixup_value_offset', '_FormatIntegerAsDecimal'), ('num_fixup_values', 'num_fixup_values', '_FormatIntegerAsDecimal'), ('logfile_sequence_number', 'logfile_sequence_number', - '_FormatIntegerAsDecimal'), + '_FormatIntegerAsDecimal'), ('virtual_cluster_number', 'virtual_cluster_number', - '_FormatIntegerAsDecimal')] + '_FormatIntegerAsDecimal')] _DEBUG_INDX_NODE_HEADER = [ ('index_values_offset', 'index_values_offset', - '_FormatIntegerAsDecimal'), + '_FormatIntegerAsDecimal'), ('index_node_size', 'index_node_size', '_FormatIntegerAsDecimal'), ('allocated_index_node_size', 'allocated_index_node_size', - '_FormatIntegerAsDecimal'), + '_FormatIntegerAsDecimal'), ('index_node_flags', 'index_node_flags', '_FormatIntegerAsDecimal')] _DEBUG_INDX_DIR_RECORD = [ @@ -54,7 +55,7 @@ class NTFSIndexI30Record(data_format.BinaryDataFile): def PrintRecord(self, record): """Prints a human readable version of the NTFS $I30 - index record. + index record. Args: record (index_directory_entry): An index_directory_entry structure. @@ -105,8 +106,7 @@ def _ParseIndexNodeHeader(self, file_object): return index_node_header, data_size def _ParseIndexDirectoryEntry(self, file_object): - """Parses an NTFS $I30 index record that contains - directory entries. + """Parses an NTFS $I30 index record that contains directory entries Args: file_object: A file-like object. @@ -139,7 +139,6 @@ def ReadFileObject(self, file_object): """ self._file_object = file_object - def ReadRecords(self): """Reads NTFS $I30 INDX records. diff --git a/tests/index_directory_entry.py b/tests/index_directory_entry.py index 8de48c6..989ec73 100644 --- a/tests/index_directory_entry.py +++ b/tests/index_directory_entry.py @@ -68,8 +68,7 @@ class IndexRecordFileTest(test_lib.BaseTestCase): }] def _GetTestStructure(self, path): - """ Helper method to create an NTFSIndexI30Record - object. + """ Helper method to create an NTFSIndexI30Record object. Args: path (str): Path to a test file.