dissect.database 1.2.dev1__tar.gz → 1.2.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. {dissect_database-1.2.dev1/dissect.database.egg-info → dissect_database-1.2.dev2}/PKG-INFO +1 -1
  2. dissect_database-1.2.dev2/dissect/database/ese/cursor.py +433 -0
  3. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/database.py +10 -3
  4. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/query.py +56 -59
  5. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/page.py +4 -4
  6. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/table.py +6 -10
  7. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2/dissect.database.egg-info}/PKG-INFO +1 -1
  8. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect.database.egg-info/SOURCES.txt +0 -1
  9. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/test_query.py +19 -8
  10. dissect_database-1.2.dev1/dissect/database/ese/btree.py +0 -177
  11. dissect_database-1.2.dev1/dissect/database/ese/cursor.py +0 -230
  12. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/COPYRIGHT +0 -0
  13. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/LICENSE +0 -0
  14. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/MANIFEST.in +0 -0
  15. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/README.md +0 -0
  16. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/__init__.py +0 -0
  17. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/__init__.py +0 -0
  18. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/c_db.py +0 -0
  19. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/c_db.pyi +0 -0
  20. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/db.py +0 -0
  21. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/tools/__init__.py +0 -0
  22. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/tools/c_rpm.py +0 -0
  23. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/tools/c_rpm.pyi +0 -0
  24. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/tools/rpm.py +0 -0
  25. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/__init__.py +0 -0
  26. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/c_ese.py +0 -0
  27. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/c_ese.pyi +0 -0
  28. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/compression.py +0 -0
  29. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ese.py +0 -0
  30. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/exception.py +0 -0
  31. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/index.py +0 -0
  32. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/lcmapstring.py +0 -0
  33. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/__init__.py +0 -0
  34. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/c_ds.py +0 -0
  35. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/c_ds.pyi +0 -0
  36. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/c_pek.py +0 -0
  37. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/c_pek.pyi +0 -0
  38. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/c_sd.py +0 -0
  39. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/c_sd.pyi +0 -0
  40. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/ntds.py +0 -0
  41. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/__init__.py +0 -0
  42. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/applicationsettings.py +0 -0
  43. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/attributeschema.py +0 -0
  44. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/builtindomain.py +0 -0
  45. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/certificationauthority.py +0 -0
  46. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/classschema.py +0 -0
  47. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/classstore.py +0 -0
  48. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/computer.py +0 -0
  49. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/configuration.py +0 -0
  50. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/container.py +0 -0
  51. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/controlaccessright.py +0 -0
  52. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/crldistributionpoint.py +0 -0
  53. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/crossref.py +0 -0
  54. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/crossrefcontainer.py +0 -0
  55. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/dfsconfiguration.py +0 -0
  56. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/displayspecifier.py +0 -0
  57. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/dmd.py +0 -0
  58. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/dnsnode.py +0 -0
  59. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/dnszone.py +0 -0
  60. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/domain.py +0 -0
  61. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/domaindns.py +0 -0
  62. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/domainpolicy.py +0 -0
  63. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/dsuisettings.py +0 -0
  64. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/filelinktracking.py +0 -0
  65. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/foreignsecurityprincipal.py +0 -0
  66. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/group.py +0 -0
  67. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/grouppolicycontainer.py +0 -0
  68. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/infrastructureupdate.py +0 -0
  69. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/intersitetransport.py +0 -0
  70. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/intersitetransportcontainer.py +0 -0
  71. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ipsecbase.py +0 -0
  72. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ipsecfilter.py +0 -0
  73. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ipsecisakmppolicy.py +0 -0
  74. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ipsecnegotiationpolicy.py +0 -0
  75. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ipsecnfa.py +0 -0
  76. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ipsecpolicy.py +0 -0
  77. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/leaf.py +0 -0
  78. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/linktrackobjectmovetable.py +0 -0
  79. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/linktrackvolumetable.py +0 -0
  80. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/locality.py +0 -0
  81. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/lostandfound.py +0 -0
  82. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msauthz_centralaccesspolicies.py +0 -0
  83. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msauthz_centralaccessrules.py +0 -0
  84. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_content.py +0 -0
  85. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_contentset.py +0 -0
  86. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_globalsettings.py +0 -0
  87. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_localsettings.py +0 -0
  88. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_member.py +0 -0
  89. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_replicationgroup.py +0 -0
  90. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_subscriber.py +0 -0
  91. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_subscription.py +0 -0
  92. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_topology.py +0 -0
  93. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdns_serversettings.py +0 -0
  94. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_authnpolicies.py +0 -0
  95. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_authnpolicysilos.py +0 -0
  96. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_claimstransformationpolicies.py +0 -0
  97. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_claimtype.py +0 -0
  98. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_claimtypepropertybase.py +0 -0
  99. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_claimtypes.py +0 -0
  100. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_optionalfeature.py +0 -0
  101. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_passwordsettingscontainer.py +0 -0
  102. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_quotacontainer.py +0 -0
  103. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_resourceproperties.py +0 -0
  104. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_resourceproperty.py +0 -0
  105. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_resourcepropertylist.py +0 -0
  106. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_shadowprincipalcontainer.py +0 -0
  107. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_valuetype.py +0 -0
  108. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msimaging_psps.py +0 -0
  109. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/mskds_provserverconfiguration.py +0 -0
  110. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msmqenterprisesettings.py +0 -0
  111. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/mspki_enterpriseoid.py +0 -0
  112. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/mspki_privatekeyrecoveryagent.py +0 -0
  113. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msspp_activationobjectscontainer.py +0 -0
  114. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/mstpm_informationobjectscontainer.py +0 -0
  115. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ntdsconnection.py +0 -0
  116. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ntdsdsa.py +0 -0
  117. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ntdsservice.py +0 -0
  118. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ntdssitesettings.py +0 -0
  119. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ntfrssettings.py +0 -0
  120. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/object.py +0 -0
  121. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/organizationalperson.py +0 -0
  122. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/organizationalunit.py +0 -0
  123. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/person.py +0 -0
  124. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/physicallocation.py +0 -0
  125. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/pkicertificatetemplate.py +0 -0
  126. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/pkienrollmentservice.py +0 -0
  127. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/querypolicy.py +0 -0
  128. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ridmanager.py +0 -0
  129. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ridset.py +0 -0
  130. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/rpccontainer.py +0 -0
  131. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/rrasadministrationdictionary.py +0 -0
  132. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/samserver.py +0 -0
  133. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/secret.py +0 -0
  134. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/securityobject.py +0 -0
  135. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/server.py +0 -0
  136. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/serverscontainer.py +0 -0
  137. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/site.py +0 -0
  138. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/sitelink.py +0 -0
  139. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/sitescontainer.py +0 -0
  140. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/subnetcontainer.py +0 -0
  141. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/subschema.py +0 -0
  142. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/top.py +0 -0
  143. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/trusteddomain.py +0 -0
  144. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/user.py +0 -0
  145. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/pek.py +0 -0
  146. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/schema.py +0 -0
  147. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/sd.py +0 -0
  148. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/tools/__init__.py +0 -0
  149. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/tools/ntds.py +0 -0
  150. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/util.py +0 -0
  151. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/record.py +0 -0
  152. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/sorting_table.py +0 -0
  153. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/tools/__init__.py +0 -0
  154. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/tools/certlog.py +0 -0
  155. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/tools/impacket.py +0 -0
  156. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/tools/sru.py +0 -0
  157. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/tools/ual.py +0 -0
  158. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/util.py +0 -0
  159. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/exception.py +0 -0
  160. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/__init__.py +0 -0
  161. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/c_sqlite3.py +0 -0
  162. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/c_sqlite3.pyi +0 -0
  163. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/encryption/__init__.py +0 -0
  164. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/encryption/sqlcipher/__init__.py +0 -0
  165. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/encryption/sqlcipher/exception.py +0 -0
  166. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/encryption/sqlcipher/sqlcipher.py +0 -0
  167. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/exception.py +0 -0
  168. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/sqlite3.py +0 -0
  169. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/util.py +0 -0
  170. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/wal.py +0 -0
  171. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect.database.egg-info/dependency_links.txt +0 -0
  172. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect.database.egg-info/entry_points.txt +0 -0
  173. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect.database.egg-info/requires.txt +0 -0
  174. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect.database.egg-info/top_level.txt +0 -0
  175. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/pyproject.toml +0 -0
  176. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/setup.cfg +0 -0
  177. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/__init__.py +0 -0
  178. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/_docs/Makefile +0 -0
  179. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/_docs/conf.py +0 -0
  180. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/_docs/index.rst +0 -0
  181. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/_tools/sqlite3/__init__.py +0 -0
  182. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/_tools/sqlite3/generate_sqlite.py +0 -0
  183. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/_util.py +0 -0
  184. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/bsd/__init__.py +0 -0
  185. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/bsd/conftest.py +0 -0
  186. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/bsd/test_db.py +0 -0
  187. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/bsd/test_rpm.py +0 -0
  188. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/conftest.py +0 -0
  189. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/__init__.py +0 -0
  190. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/conftest.py +0 -0
  191. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/__init__.py +0 -0
  192. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/conftest.py +0 -0
  193. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/test_benchmark.py +0 -0
  194. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/test_ntds.py +0 -0
  195. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/test_pek.py +0 -0
  196. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/test_schema.py +0 -0
  197. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/test_sd.py +0 -0
  198. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/test_util.py +0 -0
  199. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/test_cursor.py +0 -0
  200. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/test_ese.py +0 -0
  201. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/test_index.py +0 -0
  202. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/test_page.py +0 -0
  203. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/test_record.py +0 -0
  204. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/test_table.py +0 -0
  205. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/tools/__init__.py +0 -0
  206. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/tools/test_certlog.py +0 -0
  207. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/tools/test_sru.py +0 -0
  208. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/tools/test_ual.py +0 -0
  209. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/__init__.py +0 -0
  210. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/conftest.py +0 -0
  211. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/test_default_values.py +0 -0
  212. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/test_row.py +0 -0
  213. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/test_sqlcipher.py +0 -0
  214. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/test_sqlite3.py +0 -0
  215. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/test_util.py +0 -0
  216. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/test_wal.py +0 -0
  217. {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.database
3
- Version: 1.2.dev1
3
+ Version: 1.2.dev2
4
4
  Summary: A Dissect module implementing parsers for various database formats, including Berkeley DB, Microsofts Extensible Storage Engine (ESE) and SQLite3
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License-Expression: Apache-2.0
@@ -0,0 +1,433 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from dissect.database.ese.exception import KeyNotFoundError
6
+ from dissect.database.ese.record import Record
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Iterator
10
+
11
+ from typing_extensions import Self
12
+
13
+ from dissect.database.ese.ese import ESE
14
+ from dissect.database.ese.index import Index
15
+ from dissect.database.ese.page import Node, Page
16
+ from dissect.database.ese.util import RecordValue
17
+
18
+
19
+ class Cursor:
20
+ """A simple cursor implementation for searching the ESE indexes on their records.
21
+
22
+ Args:
23
+ index: The :class:`~dissect.database.ese.index.Index` to create the cursor for.
24
+ """
25
+
26
+ def __init__(self, index: Index):
27
+ self.index = index
28
+ self.table = index.table
29
+ self.db = index.db
30
+
31
+ self._primary = RawCursor(self.db, index.root)
32
+ self._secondary = None if index.is_primary else RawCursor(self.db, self.table.root)
33
+
34
+ def __iter__(self) -> Iterator[Record]:
35
+ if self._primary._page.is_branch:
36
+ self._primary.first()
37
+
38
+ record = self.record()
39
+ while record is not None:
40
+ yield record
41
+ record = self.next()
42
+
43
+ def _node(self) -> Node:
44
+ """Return the node the cursor is currently on. Resolves the secondary index if needed.
45
+
46
+ Returns:
47
+ A :class:`~dissect.database.ese.page.Node` object of the current node.
48
+ """
49
+ node = self._primary.node()
50
+ if self._secondary is not None:
51
+ node = self._secondary.search(node.data.tobytes(), exact=True).node()
52
+ return node
53
+
54
+ def record(self) -> Record:
55
+ """Return the record the cursor is currently on.
56
+
57
+ Returns:
58
+ A :class:`~dissect.database.ese.record.Record` object of the current record.
59
+ """
60
+ return Record(self.table, self._node())
61
+
62
+ def reset(self) -> Self:
63
+ """Reset the internal state."""
64
+ self._primary.reset()
65
+ if self._secondary:
66
+ self._secondary.reset()
67
+ return self
68
+
69
+ def next(self) -> Record | None:
70
+ """Move the cursor to the next record and return it.
71
+
72
+ Returns:
73
+ A :class:`~dissect.database.ese.record.Record` object of the next record.
74
+ """
75
+ if self._primary.next():
76
+ return self.record()
77
+ return None
78
+
79
+ def prev(self) -> Record | None:
80
+ """Move the cursor to the previous node and return it.
81
+
82
+ Returns:
83
+ A :class:`~dissect.database.ese.record.Record` object of the previous record.
84
+ """
85
+ if self._primary.prev():
86
+ return self.record()
87
+ return None
88
+
89
+ def make_key(self, *args: RecordValue, **kwargs: RecordValue) -> bytes:
90
+ """Generate a key for this index from the given values.
91
+
92
+ Args:
93
+ *args: The values to generate a key for.
94
+ **kwargs: The columns and values to generate a key for.
95
+
96
+ Returns:
97
+ The generated key as bytes.
98
+ """
99
+ if not args and not kwargs:
100
+ raise ValueError("At least one value must be provided")
101
+
102
+ if args and kwargs:
103
+ raise ValueError("Cannot mix positional and keyword arguments in make_key")
104
+
105
+ if args and not len(args) == 1 and not isinstance(args[0], list):
106
+ raise ValueError("When using positional arguments, provide a single list of values")
107
+
108
+ return self.index.make_key(args[0] if args else kwargs)
109
+
110
+ def search(self, *args: RecordValue, **kwargs: RecordValue) -> Record:
111
+ """Search the index for the requested values.
112
+
113
+ Searching modifies the cursor state. Searching again will search from the current position.
114
+ Reset the cursor with :meth:`reset` to start from the beginning.
115
+
116
+ Args:
117
+ *args: The values to search for.
118
+ **kwargs: The columns and values to search for.
119
+
120
+ Returns:
121
+ A :class:`~dissect.database.ese.record.Record` object of the found record.
122
+ """
123
+ key = self.make_key(*args, **kwargs)
124
+ return self.search_key(key, exact=True)
125
+
126
+ def search_key(self, key: bytes, exact: bool = True) -> Record:
127
+ """Search for a record with the given ``key``.
128
+
129
+ Args:
130
+ key: The key to search for.
131
+ exact: If ``True``, search for an exact match. If ``False``, sets the cursor on the
132
+ next record that is greater than or equal to the key.
133
+ """
134
+ self._primary.search(key, exact=exact)
135
+ return self.record()
136
+
137
+ def seek(self, *args: RecordValue, **kwargs: RecordValue) -> Self:
138
+ """Seek to the record with the given values.
139
+
140
+ Args:
141
+ *args: The values to seek to.
142
+ **kwargs: The columns and values to seek to.
143
+ """
144
+ key = self.make_key(*args, **kwargs)
145
+ self.search_key(key, exact=False)
146
+ return self
147
+
148
+ def seek_key(self, key: bytes) -> Self:
149
+ """Seek to the record with the given ``key``.
150
+
151
+ Args:
152
+ key: The key to seek to.
153
+ """
154
+ self._primary.search(key, exact=False)
155
+ return self
156
+
157
+ def find(self, **kwargs: RecordValue) -> Record | None:
158
+ """Find a record in the index.
159
+
160
+ This differs from :meth:`search` in that it will allow additional filtering on non-indexed columns.
161
+
162
+ Args:
163
+ **kwargs: The columns and values to search for.
164
+ """
165
+ return next(self.find_all(**kwargs), None)
166
+
167
+ def find_all(self, **kwargs: RecordValue) -> Iterator[Record]:
168
+ """Find all records in the index that match the given values.
169
+
170
+ This differs from :meth:`search` in that it will allows additional filtering on non-indexed columns.
171
+ If you only search on indexed columns, this will yield all records that match the indexed columns.
172
+
173
+ Args:
174
+ **kwargs: The columns and values to search for.
175
+ """
176
+ indexed_columns = {c.name: kwargs.pop(c.name) for c in self.index.columns}
177
+ other_columns = kwargs
178
+
179
+ # We need at least an exact match on the indexed columns
180
+ try:
181
+ self.search(**indexed_columns)
182
+ except KeyNotFoundError:
183
+ return
184
+
185
+ current_key = self._primary.node().key
186
+ while True:
187
+ # Entries with the same indexed columns are guaranteed to be adjacent
188
+ if current_key != self._primary.node().key:
189
+ break
190
+
191
+ record = self.record()
192
+ for k, v in other_columns.items():
193
+ value = record.get(k)
194
+ # If the record value is a list, we do a check based on the queried value
195
+ if isinstance(value, list):
196
+ # If the queried value is also a list, we check if they are equal
197
+ if isinstance(v, list):
198
+ if value != v:
199
+ break
200
+ # Otherwise we check if the queried value is in the record value
201
+ elif v not in value:
202
+ break
203
+ else:
204
+ if value != v:
205
+ break
206
+ else:
207
+ yield record
208
+
209
+ if not self._primary.next():
210
+ break
211
+
212
+
213
+ class RawCursor:
214
+ """A simple cursor implementation for searching the ESE B+Trees on their raw nodes.
215
+
216
+ Args:
217
+ db: An instance of :class:`~dissect.database.ese.ese.ESE`.
218
+ root: The page to open the raw cursor on.
219
+ """
220
+
221
+ def __init__(self, db: ESE, root: Page | int):
222
+ self.db = db
223
+ self.root = db.page(root) if isinstance(root, int) else root
224
+
225
+ self._page = self.root
226
+ self._idx = 0
227
+
228
+ # Stack of (page, idx, stack[:]) for traversing back up the tree when doing in-order traversal
229
+ self._stack = []
230
+
231
+ @property
232
+ def state(self) -> tuple[Page, int, list[tuple[Page, int]]]:
233
+ """Get the current cursor state."""
234
+ return self._page, self._idx, self._stack[:]
235
+
236
+ @state.setter
237
+ def state(self, value: tuple[Page, int, list[tuple[Page, int]]]) -> None:
238
+ """Set the current cursor state."""
239
+ self._page, self._idx, self._stack = value[0], value[1], value[2][:]
240
+
241
+ def reset(self) -> Self:
242
+ """Reset the cursor to the root of the B+Tree."""
243
+ self._page = self.root
244
+ self._idx = 0
245
+ self._stack = []
246
+
247
+ return self
248
+
249
+ def node(self) -> Node:
250
+ """Return the node the cursor is currently on.
251
+
252
+ Returns:
253
+ A :class:`~dissect.database.ese.page.Node` object of the current node.
254
+ """
255
+ return self._page.node(self._idx)
256
+
257
+ def first(self) -> bool:
258
+ """Move the cursor to the first leaf node in the B+Tree."""
259
+ self.reset()
260
+ while self._page.is_branch and self._page.node_count > 0:
261
+ self.push()
262
+
263
+ return self._page.node_count != 0
264
+
265
+ def last(self) -> bool:
266
+ """Move the cursor to the last leaf node in the B+Tree."""
267
+ self.reset()
268
+ while self._page.is_branch and self._page.node_count > 0:
269
+ self._idx = self._page.node_count - 1
270
+ self.push()
271
+
272
+ self._idx = self._page.node_count - 1
273
+ return self._page.node_count != 0
274
+
275
+ def next(self) -> bool:
276
+ """Move the cursor to the next leaf node."""
277
+ if self._page.is_branch:
278
+ # Treat as if we were at the first node
279
+ self.first()
280
+ return self._page.node_count != 0
281
+
282
+ if self._idx + 1 < self._page.node_count:
283
+ self._idx += 1
284
+ elif self._stack:
285
+ # End of current page, traverse to the next leaf page
286
+
287
+ # First pop until we find a page with unvisited nodes
288
+ while self._idx + 1 >= self._page.node_count:
289
+ if not self._stack:
290
+ return False
291
+ self.pop()
292
+
293
+ self._idx += 1
294
+
295
+ # Then push down to the next page
296
+ while self._page.is_branch:
297
+ self.push()
298
+ else:
299
+ return False
300
+
301
+ return True
302
+
303
+ def prev(self) -> bool:
304
+ """Move the cursor to the previous leaf node."""
305
+ if self._page.is_branch:
306
+ # Treat as if we were at the last node
307
+ self.last()
308
+ return self._page.node_count != 0
309
+
310
+ if self._idx - 1 >= 0:
311
+ self._idx -= 1
312
+ elif self._stack:
313
+ # Start of current page, traverse to the previous leaf page
314
+
315
+ # First pop until we find a page with unvisited nodes
316
+ while self._idx - 1 < 0:
317
+ if not self._stack:
318
+ # Start of B+Tree reached
319
+ return False
320
+ self.pop()
321
+
322
+ self._idx -= 1
323
+
324
+ # Then push down to the rightmost leaf
325
+ while self._page.is_branch:
326
+ self._idx = self._page.node_count - 1
327
+ self.push()
328
+ else:
329
+ # Start of B+Tree reached
330
+ return False
331
+
332
+ return True
333
+
334
+ def push(self) -> Self:
335
+ """Push down to the child page at the current index."""
336
+ child_page = self.db.page(self._page.node(self._idx).child)
337
+
338
+ self._stack.append((self._page, self._idx))
339
+ self._page = child_page
340
+ self._idx = 0
341
+
342
+ return self
343
+
344
+ def pop(self) -> Self:
345
+ """Pop back to the parent page."""
346
+ if not self._stack:
347
+ raise IndexError("Cannot pop from an empty stack")
348
+
349
+ self._page, self._idx = self._stack.pop()
350
+
351
+ return self
352
+
353
+ def walk(self) -> Iterator[Node]:
354
+ """Walk the B+Tree in order, yielding nodes."""
355
+ if self.first():
356
+ yield self.node()
357
+
358
+ while self.next():
359
+ yield self.node()
360
+
361
+ def search(self, key: bytes, *, exact: bool = True) -> Self:
362
+ """Search the tree for the given ``key``.
363
+
364
+ Moves the cursor to the matching node, or on the last node that is less than the requested key.
365
+
366
+ Args:
367
+ key: The key to search for.
368
+ exact: Whether to only return successfully on an exact match.
369
+
370
+ Raises:
371
+ KeyNotFoundError: If an ``exact`` match was requested but not found.
372
+ """
373
+ self.reset()
374
+
375
+ while self._page.is_branch:
376
+ self._idx = find_node(self._page, key, exact=False)
377
+ self.push()
378
+
379
+ self._idx = find_node(self._page, key, exact=exact)
380
+ if self._idx >= self._page.node_count or self._idx == -1:
381
+ raise KeyNotFoundError(f"Key not found: {key!r}")
382
+
383
+ return self
384
+
385
+
386
+ def find_node(page: Page, key: bytes, *, exact: bool) -> int:
387
+ """Search a page for a node matching the given key.
388
+
389
+ Referencing Extensible-Storage-Engine source, they bail out early if they find an exact match.
390
+ However, we prefer to always find the _first_ node that is greater than or equal to the key,
391
+ so we can handle cases where there are duplicate index keys. This is important for "range" searches
392
+ where we want to find all keys matching a certain prefix, and not end up somewhere in the middle of the range.
393
+
394
+ Args:
395
+ page: The page to search.
396
+ key: The key to search.
397
+ exact: Whether to only return successfully on an exact match.
398
+
399
+ Returns:
400
+ The node number of the first node that's greater than or equal to the key, or the last node on the page if
401
+ the key is larger than all nodes. If ``exact`` is ``True`` and an exact match is not found, returns -1.
402
+ """
403
+ if page.node_count == 0:
404
+ return -1
405
+
406
+ lo, hi = 0, page.node_count - 1
407
+
408
+ node = None
409
+ while lo < hi:
410
+ mid = (lo + hi) // 2
411
+ node = page.node(mid)
412
+
413
+ # It turns out that the way BTree keys are compared matches 1:1 with how Python compares bytes
414
+ # First compare data, then length
415
+ if key > node.key:
416
+ lo = mid + 1
417
+ else:
418
+ hi = mid
419
+
420
+ # Final comparison on the last node
421
+ node = page.node(lo)
422
+
423
+ if key == node.key:
424
+ if page.is_branch:
425
+ # If there's an exact match on a key on a branch page, the actual leaf nodes are in the next branch
426
+ # Page keys for branch pages appear to be non-inclusive upper bounds
427
+ lo = min(lo + 1, page.node_count - 1)
428
+
429
+ # key != node.key
430
+ elif exact:
431
+ return -1
432
+
433
+ return lo
@@ -114,10 +114,17 @@ class DataTable:
114
114
  yield (obj := stack.pop())
115
115
  stack.extend(obj.children())
116
116
 
117
- def iter(self) -> Iterator[Object]:
118
- """Iterate over all objects in the NTDS database."""
117
+ def iter(self, raw: bool = False) -> Iterator[Object]:
118
+ """Iterate over all objects in the NTDS database.
119
+
120
+ Args:
121
+ raw: Whether to return base :class:`Object` instances without upcasting to more specific types
122
+ based on the objectClass.
123
+ """
124
+ from_record = Object if raw else Object.from_record
125
+
119
126
  for record in self.table.records():
120
- yield Object.from_record(self.db, record)
127
+ yield from_record(self.db, record)
121
128
 
122
129
  def get(self, dnt: int) -> Object:
123
130
  """Retrieve an object by its Directory Number Tag (DNT) value.
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import fnmatch
3
4
  import logging
4
- from typing import TYPE_CHECKING, Any
5
+ import re
6
+ from typing import TYPE_CHECKING
5
7
 
6
8
  from dissect.util.ldap import LogicalOperator, SearchFilter
7
9
 
@@ -33,30 +35,25 @@ class Query:
33
35
  """
34
36
  yield from self._process_query(self._filter)
35
37
 
36
- def _process_query(self, filter: SearchFilter, records: list[Record] | None = None) -> Iterator[Record]:
38
+ def _process_query(self, filter: SearchFilter, records: Iterator[Record] | None = None) -> Iterator[Record]:
37
39
  """Process LDAP query recursively, handling nested logical operations.
38
40
 
39
41
  Args:
40
42
  filter: The LDAP search filter to process.
41
- records: Optional list of records to filter instead of querying the database.
43
+ records: Optional iterable of records to filter instead of querying the database.
42
44
 
43
45
  Yields:
44
46
  Records matching the search filter.
45
47
  """
46
- if not filter.is_nested():
47
- if records is None:
48
- try:
49
- yield from self._query_database(filter)
50
- except IndexError:
51
- log.debug("No records found for filter: %s", filter)
52
- else:
53
- yield from self._filter_records(filter, records)
54
- return
55
-
56
- if filter.operator == LogicalOperator.AND:
57
- yield from self._process_and_operation(filter, records)
58
- elif filter.operator == LogicalOperator.OR:
59
- yield from self._process_or_operation(filter, records)
48
+ if filter.is_nested():
49
+ if filter.operator == LogicalOperator.AND:
50
+ yield from self._process_and_operation(filter, records)
51
+ elif filter.operator == LogicalOperator.OR:
52
+ yield from self._process_or_operation(filter, records)
53
+ elif records is not None:
54
+ yield from self._filter_records(filter, records)
55
+ else:
56
+ yield from self._query_database(filter)
60
57
 
61
58
  def _query_database(self, filter: SearchFilter) -> Iterator[Record]:
62
59
  """Execute a simple LDAP filter against the database.
@@ -73,25 +70,33 @@ class Query:
73
70
 
74
71
  # Get the database index for this attribute
75
72
  if (index := self.db.data.table.find_index([schema.column])) is None:
76
- raise ValueError(f"Index for attribute {schema.column!r} not found in the NTDS database")
77
-
78
- if "*" in filter.value:
79
- # Handle wildcard searches differently
80
- if filter.value.endswith("*"):
81
- yield from _process_wildcard_tail(index, filter.value)
82
- else:
83
- raise NotImplementedError("Wildcards in the middle or start of the value are not yet supported")
73
+ # If no index is available, we have to scan the entire table
74
+ log.debug("No index found for attribute %s (%s), scanning entire table", filter.attribute, schema.column)
75
+ yield from self._filter_records(filter, self.db.data.table.records())
84
76
  else:
85
- # Exact match query
86
- encoded_value = encode_value(self.db, schema, filter.value)
87
- yield from index.cursor().find_all(**{schema.column: encoded_value})
77
+ if "*" in filter.value:
78
+ # Handle wildcard searches differently
79
+ if filter.value.endswith("*"):
80
+ yield from _process_wildcard_tail(index, filter.value)
81
+ else:
82
+ # For more complex wildcard patterns, we need to scan the index and apply the filter
83
+ log.debug(
84
+ "Complex wildcard search for attribute %s (%s), scanning entire index",
85
+ filter.attribute,
86
+ schema.column,
87
+ )
88
+ yield from self._filter_records(filter, index.cursor())
89
+ else:
90
+ # Exact match query
91
+ encoded_value = encode_value(self.db, schema, filter.value)
92
+ yield from index.cursor().find_all(**{schema.column: encoded_value})
88
93
 
89
- def _process_and_operation(self, filter: SearchFilter, records: list[Record] | None) -> Iterator[Record]:
94
+ def _process_and_operation(self, filter: SearchFilter, records: Iterator[Record] | None) -> Iterator[Record]:
90
95
  """Process AND logical operation.
91
96
 
92
97
  Args:
93
98
  filter: The LDAP search filter with AND operator.
94
- records: Optional list of records to filter.
99
+ records: Optional iterable of records to filter.
95
100
 
96
101
  Yields:
97
102
  Records matching all conditions in the AND operation.
@@ -102,19 +107,19 @@ class Query:
102
107
  else:
103
108
  # Use the first child as base query, then filter with remaining children
104
109
  base_query, *remaining_children = filter.children
105
- records_to_process = list(self._process_query(base_query))
110
+ records_to_process = self._process_query(base_query)
106
111
  children_to_check = remaining_children
107
112
 
108
113
  for record in records_to_process:
109
114
  if all(any(self._process_query(child, records=[record])) for child in children_to_check):
110
115
  yield record
111
116
 
112
- def _process_or_operation(self, filter: SearchFilter, records: list[Record] | None) -> Iterator[Record]:
117
+ def _process_or_operation(self, filter: SearchFilter, records: Iterator[Record] | None) -> Iterator[Record]:
113
118
  """Process OR logical operation.
114
119
 
115
120
  Args:
116
121
  filter: The LDAP search filter with OR operator.
117
- records: Optional list of records to filter.
122
+ records: Optional iterable of records to filter.
118
123
 
119
124
  Yields:
120
125
  Records matching any condition in the OR operation.
@@ -122,12 +127,12 @@ class Query:
122
127
  for child in filter.children:
123
128
  yield from self._process_query(child, records=records)
124
129
 
125
- def _filter_records(self, filter: SearchFilter, records: list[Record]) -> Iterator[Record]:
126
- """Filter a list of records against a simple LDAP filter.
130
+ def _filter_records(self, filter: SearchFilter, records: Iterator[Record]) -> Iterator[Record]:
131
+ """Filter an iterable of records against a simple LDAP filter.
127
132
 
128
133
  Args:
129
134
  filter: The LDAP search filter to apply.
130
- records: The list of records to filter.
135
+ records: The iterable of records to filter.
131
136
 
132
137
  Yields:
133
138
  Records that match the filter criteria.
@@ -136,14 +141,26 @@ class Query:
136
141
  return
137
142
 
138
143
  encoded_value = encode_value(self.db, schema, filter.value)
144
+ re_encoded_value = None
139
145
 
140
- has_wildcard = "*" in filter.value
141
- wildcard_prefix = filter.value.replace("*", "").lower() if has_wildcard else None
146
+ has_wildcard = "*" in filter.value and isinstance(encoded_value, str)
147
+ if has_wildcard:
148
+ re_encoded_value = re.compile(fnmatch.translate(encoded_value), re.IGNORECASE)
142
149
 
143
150
  for record in records:
144
151
  record_value = record.get(schema.column)
145
152
 
146
- if _value_matches_filter(record_value, encoded_value, has_wildcard, wildcard_prefix):
153
+ if isinstance(record_value, list):
154
+ # Currently assume that we can only search for single values, not lists
155
+ if has_wildcard and record_value and isinstance(record_value[0], str):
156
+ if any(re_encoded_value.match(rv) for rv in record_value):
157
+ yield record
158
+ elif encoded_value in record_value:
159
+ yield record
160
+
161
+ elif (
162
+ has_wildcard and isinstance(record_value, str) and re_encoded_value.match(record_value)
163
+ ) or record_value == encoded_value:
147
164
  yield record
148
165
 
149
166
 
@@ -174,26 +191,6 @@ def _process_wildcard_tail(index: Index, filter_value: str) -> Iterator[Record]:
174
191
  record = cursor.next()
175
192
 
176
193
 
177
- def _value_matches_filter(
178
- record_value: Any, encoded_value: Any, has_wildcard: bool, wildcard_prefix: str | None
179
- ) -> bool:
180
- """Return whether a record value matches the filter criteria.
181
-
182
- Args:
183
- record_value: The value from the database record.
184
- encoded_value: The encoded filter value to match against.
185
- has_wildcard: Whether the filter contains wildcard characters.
186
- wildcard_prefix: The prefix to match for wildcard searches.
187
- """
188
- if isinstance(record_value, list):
189
- return encoded_value in record_value
190
-
191
- if has_wildcard and wildcard_prefix and isinstance(record_value, str):
192
- return record_value.lower().startswith(wildcard_prefix)
193
-
194
- return encoded_value == record_value
195
-
196
-
197
194
  def _increment_last_char(value: str) -> str:
198
195
  """Increment the last character in a string to find the next lexicographically sortable key.
199
196
 
@@ -49,6 +49,9 @@ class Page:
49
49
  self._node_cls = LeafNode if self.is_leaf else BranchNode
50
50
  self._node_cache = {}
51
51
 
52
+ def __repr__(self) -> str:
53
+ return f"<Page num={self.num:d} flags={self.flags.name} nodes={self.node_count}>"
54
+
52
55
  @cached_property
53
56
  def is_small_page(self) -> bool:
54
57
  return self.db.has_small_pages
@@ -123,7 +126,7 @@ class Page:
123
126
  IndexError: If the node number is out of bounds.
124
127
  """
125
128
  if num < 0 or num > self.node_count - 1:
126
- raise IndexError(f"Node number exceeds boundaries: 0-{self.node_count - 1}")
129
+ raise IndexError(f"Node number exceeds boundaries 0-{self.node_count - 1}: {num}")
127
130
 
128
131
  if num not in self._node_cache:
129
132
  self._node_cache[num] = self._node_cls(self.tag(num + 1))
@@ -161,9 +164,6 @@ class Page:
161
164
  if self.is_root and leaf and leaf.tag.page.next_page:
162
165
  yield from db.page(leaf.tag.page.next_page).iter_leaf_nodes()
163
166
 
164
- def __repr__(self) -> str:
165
- return f"<Page num={self.num:d}>"
166
-
167
167
 
168
168
  class Tag:
169
169
  """A tag is the "physical" data entry of a page.