Skip to content

Commit 2121ea3

Browse files
committed
Fix multi values
1 parent de72858 commit 2121ea3

File tree

6 files changed

+90
-43
lines changed

6 files changed

+90
-43
lines changed

dissect/database/ese/c_ese.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@
231231
BKINFOTYPE bkinfoTypeCopyPrev; // Type of Last successful Incremental backup
232232
BKINFOTYPE bkinfoTypeDiffPrev; // Type of Last successful Differential backup
233233
234-
// 476 bytes
234+
// 476 bytes
235235
ULONG ulIncrementalReseedCount; // number of times incremental reseed has been initiated on this database
236236
LOGTIME logtimeIncrementalReseed; // the date of the last time that incremental reseed was initiated on this database
237237
ULONG ulIncrementalReseedCountOld; // number of times incremental reseed was initiated on this database before the last defrag
@@ -240,7 +240,7 @@
240240
LOGTIME logtimePagePatch; // the date of the last time that a page was patched as a part of incremental reseed
241241
ULONG ulPagePatchCountOld; // number of pages patched in the database as a part of incremental reseed before the last defrag
242242
243-
// 508 bytes
243+
// 508 bytes
244244
QWORD qwSortVersion; // DEPRECATED: In old versions had "default" (?English?) LCID version, in new versions has 0xFFFFFFFFFFFF.
245245
246246
// 516 bytes // checksum during recovery state
@@ -399,6 +399,22 @@
399399
Encrypted = 0x40, // fEncrypted
400400
};
401401
402+
flag FIELDFLAG : uint16 {
403+
NotNull = 0x0001, // NULL values not allowed
404+
Version = 0x0002, // Version field
405+
Autoincrement = 0x0004, // Autoincrement field
406+
Multivalued = 0x0008, // Multi-valued column
407+
Default = 0x0010, // Column has ISAM default value
408+
EscrowUpdate = 0x0020, // Escrow updated column
409+
Finalize = 0x0040, // Finalizable column
410+
UserDefinedDefault = 0x0080, // The default value is generated through a callback
411+
TemplateColumnESE98 = 0x0100, // Template table column created in ESE98 (ie. fDerived bit will be set in TAGFLD of records of derived tables)
412+
DeleteOnZero = 0x0200, // DeleteOnZero column
413+
PrimaryIndexPlaceholder = 0x0800, // Field is no longer in primary index, but must be retained as a placeholder
414+
Compressed = 0x1000, // Data stored in the column should be compressed
415+
Encrypted = 0x2000, // Data stored in the column is encrypted
416+
};
417+
402418
flag JET_bitIndex : uint32 {
403419
Unique = 0x00000001,
404420
Primary = 0x00000002,
@@ -467,5 +483,6 @@
467483
TAGFLD_HEADER = c_ese.TAGFLD_HEADER
468484
CODEPAGE = c_ese.CODEPAGE
469485
COMPRESSION_SCHEME = c_ese.COMPRESSION_SCHEME
486+
FIELDFLAG = c_ese.FIELDFLAG
470487
IDBFLAG = c_ese.IDBFLAG
471488
IDXFLAG = c_ese.IDXFLAG

dissect/database/ese/c_ese.pyi

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,21 @@ class _c_ese(__cs__.cstruct):
426426
Null = ...
427427
Encrypted = ...
428428

429+
class FIELDFLAG(__cs__.Flag):
430+
NotNull = ...
431+
Version = ...
432+
Autoincrement = ...
433+
Multivalued = ...
434+
Default = ...
435+
EscrowUpdate = ...
436+
Finalize = ...
437+
UserDefinedDefault = ...
438+
TemplateColumnESE98 = ...
439+
DeleteOnZero = ...
440+
PrimaryIndexPlaceholder = ...
441+
Compressed = ...
442+
Encrypted = ...
443+
429444
class JET_bitIndex(__cs__.Flag):
430445
Unique = ...
431446
Primary = ...
@@ -487,5 +502,6 @@ TAG_FLAG: TypeAlias = c_ese.TAG_FLAG
487502
TAGFLD_HEADER: TypeAlias = c_ese.TAGFLD_HEADER
488503
CODEPAGE: TypeAlias = c_ese.CODEPAGE
489504
COMPRESSION_SCHEME: TypeAlias = c_ese.COMPRESSION_SCHEME
505+
FIELDFLAG: TypeAlias = c_ese.FIELDFLAG
490506
IDBFLAG: TypeAlias = c_ese.IDBFLAG
491507
IDXFLAG: TypeAlias = c_ese.IDXFLAG

dissect/database/ese/ntds/object.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,13 +194,17 @@ def _get_attribute(db: Database, record: Record, name: str, *, raw: bool = False
194194
name: The attribute name to retrieve.
195195
raw: Whether to return the raw value without decoding.
196196
"""
197-
if (entry := db.data.schema.lookup(ldap_name=name)) is not None:
198-
column_name = entry.column_name
197+
if (schema := db.data.schema.lookup(ldap_name=name)) is not None:
198+
column_name = schema.column_name
199199
else:
200200
raise KeyError(f"Attribute not found: {name!r}")
201201

202202
value = record.get(column_name)
203203

204+
if schema.is_single_valued and isinstance(value, list):
205+
# There are a few attributes that have the flag IsSingleValued but are marked as MultiValue in ESE
206+
value = value[0]
207+
204208
if raw:
205209
return value
206210

dissect/database/ese/ntds/util.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,4 @@ def decode_value(db: Database, attribute: str, value: Any) -> Any:
299299
if decode is None:
300300
return value
301301

302-
value = [decode(db, v) for v in value] if isinstance(value, list) else decode(db, value)
303-
304-
if not schema.is_single_valued and not isinstance(value, list):
305-
value = [value]
306-
307-
return value
302+
return [decode(db, v) for v in value] if isinstance(value, list) else decode(db, value)

dissect/database/ese/record.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,10 +296,13 @@ def _parse_value(
296296

297297
parse_func = parse_func or noop
298298
if tag_field and tag_field.flags & TAGFLD_HEADER.MultiValues:
299-
value = list(map(parse_func, value))
299+
value = [parse_func(v) for v in value]
300300
else:
301301
value = parse_func(value)
302302

303+
if column.is_multivalue and not isinstance(value, list):
304+
value = [value]
305+
303306
return value
304307

305308
def _parse_multivalue(self, value: bytes, tag_field: TagField) -> list[bytes]:

dissect/database/ese/table.py

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from dissect.database.ese.btree import BTree
99
from dissect.database.ese.c_ese import (
1010
CODEPAGE,
11+
FIELDFLAG,
1112
SYSOBJ,
1213
JET_coltyp,
1314
)
@@ -239,18 +240,19 @@ def _add_index(self, index: Index) -> None:
239240

240241

241242
class Column:
242-
def __init__(self, identifier: int, name: str, type_: JET_coltyp, record: Record | None = None):
243+
def __init__(self, identifier: int, name: str, type: JET_coltyp, flags: FIELDFLAG, record: Record | None = None):
243244
self.identifier = identifier
244245
self.name = name
245-
self.type = type_
246+
self.type = type
247+
self.flags = flags
246248

247249
# Set by the table when added, only relevant for fixed value columns
248250
self._offset = None
249251

250252
self.record = record
251253

252254
def __repr__(self) -> str:
253-
return f"<Column name={self.name!r} identifier={self.identifier:#x} type={self.type} size={self.size}>"
255+
return f"<Column name={self.name!r} identifier={self.identifier:#x} type={self.type} flags={self.flags.name} size={self.size}>" # noqa: E501
254256

255257
@property
256258
def offset(self) -> int:
@@ -276,6 +278,10 @@ def is_text(self) -> bool:
276278
def is_binary(self) -> bool:
277279
return self.type in (JET_coltyp.Binary, JET_coltyp.LongBinary)
278280

281+
@cached_property
282+
def is_multivalue(self) -> bool:
283+
return bool(self.flags & FIELDFLAG.Multivalued)
284+
279285
@cached_property
280286
def size(self) -> int:
281287
if self.record and self.record.get("SpaceUsage"):
@@ -310,34 +316,34 @@ class Catalog:
310316
"""
311317

312318
CATALOG_COLUMNS = (
313-
Column(1, "ObjidTable", JET_coltyp.Long),
314-
Column(2, "Type", JET_coltyp.Short),
315-
Column(3, "Id", JET_coltyp.Long),
316-
Column(4, "ColtypOrPgnoFDP", JET_coltyp.Long),
317-
Column(5, "SpaceUsage", JET_coltyp.Long),
318-
Column(6, "Flags", JET_coltyp.Long),
319-
Column(7, "PagesOrLocale", JET_coltyp.Long),
320-
Column(8, "RootFlag", JET_coltyp.Bit),
321-
Column(9, "RecordOffset", JET_coltyp.Short),
322-
Column(10, "LCMapFlags", JET_coltyp.Long),
323-
Column(11, "KeyMost", JET_coltyp.UnsignedShort),
324-
Column(12, "LVChunkMax", JET_coltyp.Long),
325-
Column(128, "Name", JET_coltyp.Text),
326-
Column(129, "Stats", JET_coltyp.Binary),
327-
Column(130, "TemplateTable", JET_coltyp.Text),
328-
Column(131, "DefaultValue", JET_coltyp.Binary),
329-
Column(132, "KeyFldIDs", JET_coltyp.Binary),
330-
Column(133, "VarSegMac", JET_coltyp.Binary),
331-
Column(134, "ConditionalColumns", JET_coltyp.Binary),
332-
Column(135, "TupleLimits", JET_coltyp.Binary),
333-
Column(136, "Version", JET_coltyp.Binary),
334-
Column(137, "SortID", JET_coltyp.Binary),
335-
Column(256, "CallbackData", JET_coltyp.LongBinary),
336-
Column(257, "CallbackDependencies", JET_coltyp.LongBinary),
337-
Column(258, "SeparateLV", JET_coltyp.LongBinary),
338-
Column(259, "SpaceHints", JET_coltyp.LongBinary),
339-
Column(260, "SpaceDeferredLVHints", JET_coltyp.LongBinary),
340-
Column(261, "LocaleName", JET_coltyp.LongBinary),
319+
Column(1, "ObjidTable", JET_coltyp.Long, FIELDFLAG.NotNull),
320+
Column(2, "Type", JET_coltyp.Short, FIELDFLAG.NotNull),
321+
Column(3, "Id", JET_coltyp.Long, FIELDFLAG.NotNull),
322+
Column(4, "ColtypOrPgnoFDP", JET_coltyp.Long, FIELDFLAG.NotNull),
323+
Column(5, "SpaceUsage", JET_coltyp.Long, FIELDFLAG.NotNull),
324+
Column(6, "Flags", JET_coltyp.Long, FIELDFLAG.NotNull),
325+
Column(7, "PagesOrLocale", JET_coltyp.Long, FIELDFLAG.NotNull),
326+
Column(8, "RootFlag", JET_coltyp.Bit, FIELDFLAG(0)),
327+
Column(9, "RecordOffset", JET_coltyp.Short, FIELDFLAG(0)),
328+
Column(10, "LCMapFlags", JET_coltyp.Long, FIELDFLAG(0)),
329+
Column(11, "KeyMost", JET_coltyp.UnsignedShort, FIELDFLAG(0)),
330+
Column(12, "LVChunkMax", JET_coltyp.Long, FIELDFLAG(0)),
331+
Column(128, "Name", JET_coltyp.Text, FIELDFLAG.NotNull),
332+
Column(129, "Stats", JET_coltyp.Binary, FIELDFLAG(0)),
333+
Column(130, "TemplateTable", JET_coltyp.Text, FIELDFLAG(0)),
334+
Column(131, "DefaultValue", JET_coltyp.Binary, FIELDFLAG(0)),
335+
Column(132, "KeyFldIDs", JET_coltyp.Binary, FIELDFLAG(0)),
336+
Column(133, "VarSegMac", JET_coltyp.Binary, FIELDFLAG(0)),
337+
Column(134, "ConditionalColumns", JET_coltyp.Binary, FIELDFLAG(0)),
338+
Column(135, "TupleLimits", JET_coltyp.Binary, FIELDFLAG(0)),
339+
Column(136, "Version", JET_coltyp.Binary, FIELDFLAG(0)),
340+
Column(137, "SortID", JET_coltyp.Binary, FIELDFLAG(0)),
341+
Column(256, "CallbackData", JET_coltyp.LongBinary, FIELDFLAG(0)),
342+
Column(257, "CallbackDependencies", JET_coltyp.LongBinary, FIELDFLAG(0)),
343+
Column(258, "SeparateLV", JET_coltyp.LongBinary, FIELDFLAG(0)),
344+
Column(259, "SpaceHints", JET_coltyp.LongBinary, FIELDFLAG(0)),
345+
Column(260, "SpaceDeferredLVHints", JET_coltyp.LongBinary, FIELDFLAG(0)),
346+
Column(261, "LocaleName", JET_coltyp.LongBinary, FIELDFLAG(0)),
341347
)
342348

343349
def __init__(self, db: ESE, root_page: Page):
@@ -358,7 +364,13 @@ def __init__(self, db: ESE, root_page: Page):
358364
self._table_name_map[rec.get("Name")] = cur_table
359365

360366
elif rtype == SYSOBJ.Column:
361-
column = Column(rec.get("Id"), rec.get("Name"), JET_coltyp(rec.get("ColtypOrPgnoFDP")), record=rec)
367+
column = Column(
368+
rec.get("Id"),
369+
rec.get("Name"),
370+
JET_coltyp(rec.get("ColtypOrPgnoFDP")),
371+
FIELDFLAG(rec.get("Flags")),
372+
record=rec,
373+
)
362374
cur_table._add_column(column)
363375

364376
elif rtype == SYSOBJ.Index:

0 commit comments

Comments
 (0)