Skip to content

Commit c0de1f5

Browse files
committed
Minor enhancements
- efficient ntfs_get_free_clusters - Boot records directly sizes of Cluster, Record, Index and MFT LCN - more internal documentation - Security_Descriptor attribute
1 parent 7fc5fc1 commit c0de1f5

File tree

8 files changed

+83
-61
lines changed

8 files changed

+83
-61
lines changed

FATtools/NTFS/Attribute.py

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
DEBUG=int(os.getenv('FATTOOLS_DEBUG', '0'))
99

1010
__all__ = ['Attribute', 'Standard_Information', 'Attribute_List', 'Attribute_Listed', 'File_Name', 'Data',
11-
'Index_Root', 'Index_Allocation', 'Bitmap', 'Volume_Name', 'Volume_Information', 'attributes_by_id', 'attributes_by_name']
11+
'Index_Root', 'Index_Allocation', 'Bitmap', 'Volume_Name', 'Volume_Information', 'Security_Descriptor',
12+
'attributes_by_id', 'attributes_by_name']
1213

1314

1415
attributes_by_id ={
@@ -49,15 +50,16 @@ class Attribute:
4950
0x16: ('uchFlags', 'B'),
5051
0x17: ('uchPadding', 'B') } # Size = 0x18 (24) bytes (total)
5152

52-
layout_nonresident = { # additional layout (non resident content)
53-
0x10: ('u64StartVCN', '<Q'),
54-
0x18: ('u64EndVCN', '<Q'),
53+
layout_nonresident = { # additional layout (non resident contents)
54+
0x10: ('u64StartVCN', '<Q'), # first Virtual Cluster Number of contents
55+
0x18: ('u64EndVCN', '<Q'), # last VCN of contents
5556
0x20: ('wDatarunOffset', '<H'),
5657
0x22: ('wCompressionSize', '<H'),
5758
0x24: ('uchPadding', '4s'),
58-
0x28: ('u64AllocSize', '<Q'),
59-
0x30: ('u64RealSize', '<Q'),
60-
0x38: ('u64StreamSize', '<Q') } # Size = 0x40 (64) bytes (total)
59+
0x28: ('u64AllocSize', '<Q'), # bytes occupied by allocated clusters
60+
0x30: ('u64RealSize', '<Q'), # bytes occupied by true contents
61+
0x38: ('u64StreamSize', '<Q') # always equal to u64RealSize?
62+
} # Size = 0x40 (64) bytes (total)
6163

6264
def __init__(self, parent, offset):
6365
self._parent = parent # parent Record
@@ -91,10 +93,10 @@ class Standard_Information(Attribute):
9193
0x24: ('dwMaxVerNum', '<I'),
9294
0x28: ('dwVerNum', '<I'),
9395
0x2C: ('dwClassId', '<I'),
94-
0x30: ('dwOwnerId', '<I'), # these 4 since NTFS 3.0 (Windows 2000)
96+
0x30: ('dwOwnerId', '<I'), # next 4 since NTFS 3.0 (Windows 2000)
9597
0x34: ('dwSecurityId', '<I'),
9698
0x38: ('u64QuotaCharged', '<Q'),
97-
0x40: ('u64USN', '<Q') } # 0x48 (72) bytes
99+
0x40: ('u64USN', '<Q') } # 0x30 (48) - 0x48 (72) bytes
98100

99101
def __init__(self, parent, offset):
100102
Attribute.__init__(self, parent, offset)
@@ -104,6 +106,8 @@ def __str__ (self):
104106
s = ''
105107
L1 = utils.class2str(self, "$STANDARD_INFORMATION @%x\n" % self._i).split('\n')
106108
L2 = []
109+
if self.dwLength == 0x30: # NTFS <3.0
110+
del L1[-5:-1]
107111
for key in (0x18, 0x20, 0x28, 0x30):
108112
o = self._kv[key][0]
109113
v = getattr(self, o)
@@ -151,7 +155,7 @@ def __init__(self, parent, offset):
151155
self.Name = ''
152156
if self.ucbNameLen:
153157
i = self._i+self.ucbNameOffs
154-
self.Name = (b'\xFF\xFE' + self._buf[i:i+self.ucbNameLen*2]).decode('utf16')
158+
self.Name = (self._buf[i:i+self.ucbNameLen*2]).decode('utf-16le')
155159

156160
__getattr__ = utils.common_getattr
157161

@@ -169,7 +173,7 @@ class File_Name(Attribute):
169173
0x28: ('u64AllocatedSize', '<Q'),
170174
0x30: ('u64RealSize', '<Q'),
171175
0x38: ('dwFlags', '<I'),
172-
0x3C: ('dwEA', '<I'), # These 3 since NTFS 3.0 (Windows 2000)
176+
0x3C: ('dwEA', '<I'),
173177
0x40: ('ucbFileName', 'B'),
174178
0x41: ('uFileNameNamespace', 'B') } # 0x42 (66) bytes
175179

@@ -178,7 +182,7 @@ def __init__(self, parent, offset):
178182
common_update_and_swap(self)
179183
# Always resident - so name is at (24+66) bytes from start
180184
i = self._i+90
181-
self.FileName = (b'\xFF\xFE' + self._buf[i:i+self.ucbFileName*2]).decode('utf16')
185+
self.FileName = (self._buf[i:i+self.ucbFileName*2]).decode('utf-16le')
182186

183187
def __str__ (self):
184188
s = ''
@@ -260,10 +264,10 @@ def __str__ (self):
260264

261265
decode = common_dataruns_decode
262266

263-
""" A Bitmap is typically stored:
267+
""" A Bitmap is typically found:
264268
- as $MFT Record Attribute (tracking used FILE records)
265-
- as $INDEX_ALLOCATION contents (tracking FILE records *not* used)
266-
- as $Bitmap record contents (tracking free volume clusters) """
269+
- as directory record attribute (tracking INDX blocks used in an $INDEX_ALLOCATION)
270+
- as $Bitmap file contents (tracking free/used volume clusters)"""
267271
class Bitmap(Attribute):
268272
specific_layout = {}
269273

@@ -298,7 +302,7 @@ def __init__(self, parent, offset):
298302
Attribute.__init__(self, parent, offset)
299303
common_update_and_swap(self)
300304
i = self._i + self.wAttrOffset
301-
self.VolumeName = (b'\xFF\xFE' + self._buf[i:i+self.dwLength]).decode('utf16')
305+
self.VolumeName = (self._buf[i:i+self.dwLength]).decode('utf-16le')
302306

303307
def __str__ (self):
304308
L1 = utils.class2str(self, "$VOLUME_NAME @%x\n" % self._i).split('\n')
@@ -308,7 +312,7 @@ class Volume_Information(Attribute):
308312
# Always resident
309313
specific_layout = {
310314
0x00: ('u64Reserved', '<Q'),
311-
0x08: ('bMajorVersion', 'B'), # 3.1=XP+, 3.0=2K, 1.2=NT
315+
0x08: ('bMajorVersion', 'B'), # 1.1=NT 3.51, 1.2=NT4, 3.0=2K, 3.1=XP+
312316
0x09: ('bMinorVersion', 'B'),
313317
0x0A: ('wFlags', '<H'), # 1=dirty, 2=log resize, 4=to upgrade, 8=mounted on NT4, 10h=del USN, 20h=repair OIDS, 8000h=modified by CHKDSK
314318
0x0C: ('dwReserved', '<I') } # 0x10 (16) bytes
@@ -318,3 +322,15 @@ def __init__(self, parent, offset):
318322
common_update_and_swap(self)
319323

320324
def __str__ (self): return utils.class2str(self, "$VOLUME_INFORMATION @%x\n" % self._i)
325+
326+
def is_dirty(self):
327+
return self.wFlags & 1
328+
329+
class Security_Descriptor(Attribute):
330+
specific_layout = {}
331+
332+
def __init__(self, parent, offset):
333+
Attribute.__init__(self, parent, offset)
334+
common_update_and_swap(self)
335+
336+
def __str__ (self): return utils.class2str(self, "$SECURITY_DESCRIPTOR @%x\n" % self._i)

FATtools/NTFS/Boot.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,26 @@
33

44
class Bootsector:
55
layout = { # { offset: (name, unpack string) }
6-
0x00: ('chJumpInstruction', '3s'),
7-
0x03: ('chOemID', '8s'),
8-
0x0B: ('wBytesPerSec', '<H'),
9-
0x0D: ('uchSecPerClust', 'B'),
10-
0x0E: ('u64Unused1', '<Q'),
11-
0x15: ('uchMediaDescriptor', 'B'),
6+
0x00: ('chJumpInstruction', '3s'), # CHKDSK likes this
7+
0x03: ('chOemID', '8s'), # "NTFS "
8+
0x0B: ('wBytesPerSec', '<H'), # typically, 512
9+
0x0D: ('uchSecPerClust', 'B'), # typically, 8 (4K cluster)
10+
0x0E: ('u64Unused1', '<Q'), # 0xf800000000000000
11+
0x15: ('uchMediaDescriptor', 'B'), # typically 0xF8 (HDD)
1212
0x16: ('wUnused2', '<H'),
1313
0x18: ('wSecPerTrack', '<H'),
1414
0x1A: ('wNumberOfHeads', '<H'),
1515
0x1C: ('u64Unused3', '<Q'),
16-
0x24: ('dwUnknown', '<I'),
16+
0x24: ('dwUnknown', '<I'), # typically 0x800080
1717
0x25: ('dwUnused4', '<I'),
18-
0x28: ('u64TotalSectors', '<Q'),
19-
0x30: ('u64MFTLogicalClustNum', '<Q'),
20-
0x38: ('u64MFTMirrLogicalClustNum', '<Q'),
21-
0x40: ('nClustPerMFTRecord', '<b'), # if <0, 2^-n
18+
0x28: ('u64TotalSectors', '<Q'), # count of volume sectors
19+
0x30: ('u64MFTLogicalClustNum', '<Q'), # cluster of the $MFT Record (and Master File Table start)
20+
0x38: ('u64MFTMirrLogicalClustNum', '<Q'), # $MFTMirr cluster (backup of first 4 $MFT Record)
21+
0x40: ('nClustPerMFTRecord', '<b'), # if n<0 then 2^-n. Typically, 0xF6 (-10) or 1K.
2222
0x44: ('nClustPerIndexRecord', '<b'),
2323
0x48: ('u64VolumeSerialNum', '<Q'),
24-
#~ 0x50: ('dwChecksum', '<I'),
25-
#~ 0x54: ('chBootstrapCode', '426s'),
24+
0x50: ('dwChecksum', '<I'), # typically zero
25+
0x54: ('chBootstrapCode', '426s'),
2626
0x1FE: ('wSecMark', '<H') } # Size = 0x100 (512 byte)
2727

2828
def __init__ (self, stream):
@@ -35,6 +35,11 @@ def __init__ (self, stream):
3535
self._vk = {} # { name: offset}
3636
for k, v in self._kv.items():
3737
self._vk[v[0]] = k
38+
# calculates and stores immediately some vital numbers
39+
self.LcnMFT = self.mft()
40+
self.cbCluster = self.cluster()
41+
self.cbRecord = self.record()
42+
self.cbIndx = self.index()
3843

3944
__getattr__ = utils.common_getattr
4045

FATtools/NTFS/Commons.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def common_dataruns_decode(self):
6868
# computates and stores run length and offset according to cluster size
6969
if DEBUG&8: log("length=%d offset=%d prevoffset=%d", length, offset, self.dataruns[-1])
7070
# CAVE! EFFECTIVE cluster size MUST be used!
71-
self.dataruns += (length* self.boot.cluster(), (offset* self.boot.cluster()+self.dataruns[-1]))
71+
self.dataruns += (length* self.boot.cbCluster, (offset* self.boot.cbCluster+self.dataruns[-1]))
7272
i += n_offset
7373
if DEBUG&8: log("decoded dataruns @%d:\n%s", self._i, self.dataruns)
7474

FATtools/NTFS/Index.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def __init__ (self, buffer, index):
189189
self.FileName = ''
190190
if self.wfilenameOffset:
191191
j = index + 82
192-
self.FileName = (b'\xFF\xFE' + self._buf[j: j+self.ucbFileName*2]).decode('utf16')
192+
self.FileName = (self._buf[j: j+self.ucbFileName*2]).decode('utf-16le')
193193
if DEBUG & 8: log('Decoded INDEX_ENTRY @%x\n%s', index, self)
194194

195195
__getattr__ = utils.common_getattr

FATtools/NTFS/Record.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,20 @@ class Record:
1717
0x10: ('wSequence', '<H'),
1818
0x12: ('wHardLinks', '<H'),
1919
0x14: ('wAttribOffset', '<H'),
20-
0x16: ('wFlags', '<H'),
20+
0x16: ('wFlags', '<H'), # 1=Record in use
2121
0x18: ('dwRecLength', '<I'),
2222
0x1C: ('dwAllLength', '<I'),
2323
0x20: ('u64BaseMftRec', '<Q'),
2424
0x28: ('wNextAttrID', '<H'),
2525
0x2A: ('wFixupPattern', '<H'),
26-
0x2C: ('dwMFTRecNumber', '<I') } # Size = 0x30 (48 byte)
27-
26+
0x2C: ('dwMFTRecNumber', '<I') # NTFS v3.0+
27+
} # Size = 0x2C (44 byte)
28+
2829
def __init__ (self, boot, mftstream):
2930
#~ mftrecord.boot.stream --> NTFS Volume stream
3031
#~ mftrecord._stream --> $MFT stream
3132
self.boot = boot
32-
record_size = boot.record()
33+
record_size = boot.cbRecord
3334
self._i = 0 # buffer offset
3435
self._pos = mftstream.tell() # MFT offset
3536
self._buf = mftstream.read(record_size)
@@ -62,6 +63,8 @@ def __init__ (self, boot, mftstream):
6263
self._expand_attribute_list(a)
6364
elif dwType == 0x30:
6465
a = File_Name(self, offset)
66+
elif dwType == 0x50:
67+
a = Security_Descriptor(self, offset)
6568
elif dwType == 0x60:
6669
a = Volume_Name(self, offset)
6770
elif dwType == 0x70:
@@ -94,11 +97,11 @@ def __init__ (self, boot, mftstream):
9497
__getattr__ = utils.common_getattr
9598
fixup = common_fixup
9699

97-
def __str__ (self): return utils.class2str(self, "MFT Record 0x%X @%x\n" % (self._pos//self.boot.record(), self._pos))
100+
def __str__ (self): return utils.class2str(self, "MFT Record 0x%X @%x\n" % (self._pos//self.boot.cbRecord, self._pos))
98101

99102
def next(self, index=1):
100103
"Parses the next or n-th Record"
101-
off = index * self.boot.record()
104+
off = index * self.boot.cbRecord
102105
if index > 1:
103106
if off > self._stream.size:
104107
raise NTFSException("MFT Record #%d does not exist!" % index)
@@ -131,7 +134,7 @@ def _expand_attribute_list(self, al):
131134
for a in expanded:
132135
n = a.u64BaseMFTFileRef & 0x0000FFFFFFFFFFFF
133136
# this listed attributed resides in this same Record
134-
if n*self.boot.record() == self._pos:
137+
if n*self.boot.cbRecord == self._pos:
135138
continue
136139
if DEBUG&8: log("loading attributes in Record %08X", n)
137140
# loads the referenced Record and updates with contained attributes

FATtools/NTFS/Utils.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,9 @@ def iterator(p):
149149

150150
def getdiskspace(p):
151151
"Returns the disk free space in a tuple (clusters, bytes)"
152-
return (0, 0)
153-
#~ freec = ntfs_get_free_clusters(p.mft)
154-
#~ return (freec, freec*p.mft.boot.cluster())
152+
#~ return (0, 0)
153+
freec = ntfs_get_free_clusters(p.mft)
154+
return (freec, freec*p.mft.boot.cbCluster)
155155

156156

157157

@@ -170,46 +170,44 @@ def ntfs_open_volume(stream):
170170
boot = Bootsector(stream)
171171
assert boot.chOemID == b'NTFS '
172172
assert boot.wSecMark == 0xaa55
173-
stream.seek(boot.mft())
173+
stream.seek(boot.LcnMFT)
174174
mft = Record(boot, stream)
175175
if mft.find_attribute("$FILE_NAME")[0].FileName == '$MFT':
176176
mft = Record(boot, mft.find_attribute(0x80)[0].file)
177177
else:
178178
raise NTFSException("The NTFS Master File Table $MFT was not found!")
179179
return mft
180180

181-
# Terribly slow! Avoid using!
182181
def ntfs_get_free_clusters(mft):
183-
"Computates free clusters given the MFT Record"
182+
# Precalcola il numero di zero per ogni byte
183+
bit_zero_table = [8 - bin(i).count('1') for i in range(256)]
184184
bmp = ntfs_open_record(mft, "$Bitmap") # NTFS volume bitmap
185185
f = bmp.find_attribute("$DATA")[-1].file
186186
totc = mft.boot.u64TotalSectors // mft.boot.uchSecPerClust # volume clusters
187-
#~ print('DBG: total clusters', totc)
188-
#~ print('DBG: $Bitmap size', f.size)
189187
freec = 0
188+
excb = 8 - totc%8 # excedent bits
190189
while totc > 0:
191-
B = f.read(1) # process 8 clusters at once
192-
totc -= 8
193-
if B == b'\x00':
194-
freec += 8
195-
continue
196-
if B == b'\xFF':
197-
continue
198-
for i in range(8):
199-
if not ((B[0] & (1 << i)) != 0): freec += 1
190+
cb = min(totc//8 or 1, 1<<20) # process min 1b, max 1MB bitmap
191+
totc -= cb*8
192+
buf = f.read(cb)
193+
zeros = sum(bit_zero_table[b] for b in buf)
194+
freec += zeros
195+
if totc < 0:
196+
# excedent bits are 1 already?
197+
pass
200198
return freec
201199

202200
def ntfs_open_dir(rcrd):
203201
"Opens a directory record returning its associated Index"
204202
# Entries can be both resident and not resident
205203
ir = rcrd.find_attribute("$INDEX_ROOT")[0]
206-
ixr = Index(ir.file, None, rcrd.boot.index(), 1)
204+
ixr = Index(ir.file, None, rcrd.boot.cbIndx, 1)
207205
ia = rcrd.find_attribute("$INDEX_ALLOCATION")
208206
# An allocation, if present, extends the root index
209207
if ia:
210208
ia = ia[0]
211209
bm = rcrd.find_attribute("$BITMAP")[0]
212-
ixa = Index(ia.file, bm, rcrd.boot.index(), 0)
210+
ixa = Index(ia.file, bm, rcrd.boot.cbIndx, 0)
213211
return IndexGroup(ixr, ixa)
214212
return ixr
215213

@@ -221,7 +219,7 @@ def ntfs_open_root(rcrd):
221219
def ntfs_open_record(mftrecord, record):
222220
"Opens a MFT Record by name or number, directly searching the MFT"
223221
if type(record) == int:
224-
mftrecord._stream.seek(record*mftrecord.boot.record())
222+
mftrecord._stream.seek(record*mftrecord.boot.cbRecord)
225223
return Record(mftrecord.boot, mftrecord._stream)
226224
elif type(record) == str:
227225
mftrecord._stream.seek(0)

FATtools/NTFS/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
COPYRIGHT = '''Copyright (C)2012-2025, by maxpat78. GNU GPL v3 applies.
55
This free software reads NTFS file systems WITH ABSOLUTELY NO WARRANTY!'''
66

7-
VERSION = '0.9.2b'
7+
VERSION = '0.9.3b'

FATtools/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.1.2'
1+
__version__ = '1.1.3'

0 commit comments

Comments
 (0)