pyOpenVBA 6.4.0__tar.gz → 6.5.0__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 (236) hide show
  1. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/PKG-INFO +52 -1
  2. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/README.md +51 -0
  3. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/pyproject.toml +1 -1
  4. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/__init__.py +10 -1
  5. pyopenvba-6.5.0/src/pyopenvba/integrity.py +506 -0
  6. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/.gitignore +0 -0
  7. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/LICENSE.md +0 -0
  8. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/architecture.md +0 -0
  9. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/excel_checklist.csv +0 -0
  10. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/host_completeness.md +0 -0
  11. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/ms-ovba-implementation-guide_v2.md +0 -0
  12. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/power_query.md +0 -0
  13. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/research/access_complex/README.md +0 -0
  14. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/research/access_designs/README.md +0 -0
  15. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/research/access_macros/README.md +0 -0
  16. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/research/access_write/README.md +0 -0
  17. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/research/pcode/README.md +0 -0
  18. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/roadmap.md +0 -0
  19. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/vba_runtime.md +0 -0
  20. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/__main__.py +0 -0
  21. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_a1.py +0 -0
  22. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_deflate.py +0 -0
  23. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_host.py +0 -0
  24. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_new_project.py +0 -0
  25. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_oforms_pages.py +0 -0
  26. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_oforms_records.py +0 -0
  27. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_package_copy.py +0 -0
  28. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_package_signature.py +0 -0
  29. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_ppt_container.py +0 -0
  30. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_references.py +0 -0
  31. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_relationships.py +0 -0
  32. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/__init__.py +0 -0
  33. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
  34. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
  35. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
  36. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
  37. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
  38. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
  39. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
  40. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx +0 -0
  41. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
  42. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
  43. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/form.blob +0 -0
  44. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/form.lvprop +0 -0
  45. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/form.prototypes +0 -0
  46. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
  47. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/report.blob +0 -0
  48. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/report.lvprop +0 -0
  49. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/report.prototypes +0 -0
  50. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
  51. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_xml.py +0 -0
  52. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/__init__.py +0 -0
  53. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_alloc.py +0 -0
  54. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_btree.py +0 -0
  55. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_collation.py +0 -0
  56. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_collation_general_legacy.py +0 -0
  57. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_compact.py +0 -0
  58. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_complex.py +0 -0
  59. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_datapage.py +0 -0
  60. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_ddl.py +0 -0
  61. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_designs.py +0 -0
  62. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_facade.py +0 -0
  63. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_format.py +0 -0
  64. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_index.py +0 -0
  65. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_lval.py +0 -0
  66. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_macros.py +0 -0
  67. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_pages.py +0 -0
  68. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_props.py +0 -0
  69. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_queries.py +0 -0
  70. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_rows.py +0 -0
  71. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_schema.py +0 -0
  72. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_sql.py +0 -0
  73. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_storage.py +0 -0
  74. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_tdef.py +0 -0
  75. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_themes.py +0 -0
  76. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_validate.py +0 -0
  77. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_vba.py +0 -0
  78. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/database.py +0 -0
  79. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access_read.py +0 -0
  80. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/__init__.py +0 -0
  81. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/__init__.py +0 -0
  82. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_arrays.py +0 -0
  83. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_autofill.py +0 -0
  84. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_autofilter.py +0 -0
  85. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_bridge.py +0 -0
  86. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_calc.py +0 -0
  87. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_cell_styles.py +0 -0
  88. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_clipboard.py +0 -0
  89. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_control_refs.py +0 -0
  90. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_controls.py +0 -0
  91. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_copy_names.py +0 -0
  92. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_dimensions.py +0 -0
  93. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_duplicates.py +0 -0
  94. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_dynamic_file.py +0 -0
  95. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_editing.py +0 -0
  96. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_engine_book.py +0 -0
  97. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_evaluate.py +0 -0
  98. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_events.py +0 -0
  99. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_find.py +0 -0
  100. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_font_rows.py +0 -0
  101. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_formats.py +0 -0
  102. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_formula_format.py +0 -0
  103. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_hyperlinks.py +0 -0
  104. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_hyperlinks_file.py +0 -0
  105. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_io.py +0 -0
  106. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_merges.py +0 -0
  107. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_model.py +0 -0
  108. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_named_api.py +0 -0
  109. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_names.py +0 -0
  110. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_notes.py +0 -0
  111. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_notes_file.py +0 -0
  112. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_number_format.py +0 -0
  113. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_protection.py +0 -0
  114. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_radios.py +0 -0
  115. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_refresh.py +0 -0
  116. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_region.py +0 -0
  117. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_row_formats.py +0 -0
  118. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_shape_api.py +0 -0
  119. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_shapes.py +0 -0
  120. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_shared.py +0 -0
  121. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_sheet_copy.py +0 -0
  122. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_sheet_move.py +0 -0
  123. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_sort.py +0 -0
  124. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_special_cells.py +0 -0
  125. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_spills.py +0 -0
  126. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_styles.py +0 -0
  127. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_table_edits.py +0 -0
  128. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_tables.py +0 -0
  129. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_text_columns.py +0 -0
  130. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_typing.py +0 -0
  131. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_validation.py +0 -0
  132. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_visible.py +0 -0
  133. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_windows.py +0 -0
  134. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_worksheet_functions.py +0 -0
  135. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/powerpoint/__init__.py +0 -0
  136. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/powerpoint/_bridge.py +0 -0
  137. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/powerpoint/_io.py +0 -0
  138. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/powerpoint/_model.py +0 -0
  139. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/word/__init__.py +0 -0
  140. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/word/_bridge.py +0 -0
  141. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/word/_io.py +0 -0
  142. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/word/_model.py +0 -0
  143. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/cfb.py +0 -0
  144. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/excel.py +0 -0
  145. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/exceptions.py +0 -0
  146. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/forms.py +0 -0
  147. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/__init__.py +0 -0
  148. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_arity.py +0 -0
  149. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/__init__.py +0 -0
  150. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/catalog.py +0 -0
  151. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/cells.py +0 -0
  152. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/collate.py +0 -0
  153. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/dates.py +0 -0
  154. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/evaluator.py +0 -0
  155. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/__init__.py +0 -0
  156. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/aggregate.py +0 -0
  157. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/arithmetic.py +0 -0
  158. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/arrays.py +0 -0
  159. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/common.py +0 -0
  160. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/conditional.py +0 -0
  161. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/database.py +0 -0
  162. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/dates.py +0 -0
  163. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/distributions.py +0 -0
  164. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/engineering.py +0 -0
  165. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/financial.py +0 -0
  166. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/information.py +0 -0
  167. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/lambdas.py +0 -0
  168. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/logical.py +0 -0
  169. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/lookup.py +0 -0
  170. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/matrix.py +0 -0
  171. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/statistics.py +0 -0
  172. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/subtotal.py +0 -0
  173. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/text.py +0 -0
  174. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/host.py +0 -0
  175. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/lexer.py +0 -0
  176. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/nodes.py +0 -0
  177. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/numbers.py +0 -0
  178. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/parser.py +0 -0
  179. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/precise.py +0 -0
  180. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/reference.py +0 -0
  181. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/registry.py +0 -0
  182. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/special.py +0 -0
  183. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/values.py +0 -0
  184. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_deep.py +0 -0
  185. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_display.py +0 -0
  186. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_formula2.py +0 -0
  187. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_inventory.py +0 -0
  188. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_parse.py +0 -0
  189. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_prefixes.py +0 -0
  190. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_r1c1.py +0 -0
  191. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_spell.py +0 -0
  192. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_structured.py +0 -0
  193. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_values.py +0 -0
  194. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/__init__.py +0 -0
  195. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_ast.py +0 -0
  196. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_constants_data.py +0 -0
  197. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_intrinsics.py +0 -0
  198. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_inventory.py +0 -0
  199. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_inventory_data.py +0 -0
  200. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_lex.py +0 -0
  201. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_objects.py +0 -0
  202. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_parse.py +0 -0
  203. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_preprocess.py +0 -0
  204. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_runtime.py +0 -0
  205. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_scripting.py +0 -0
  206. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_values.py +0 -0
  207. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/__init__.py +0 -0
  208. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_eval.py +0 -0
  209. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_inventory.py +0 -0
  210. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_inventory_data.py +0 -0
  211. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_library.py +0 -0
  212. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_parse.py +0 -0
  213. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_values.py +0 -0
  214. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerpoint.py +0 -0
  215. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/__init__.py +0 -0
  216. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_binary.py +0 -0
  217. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_files.py +0 -0
  218. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_mashup.py +0 -0
  219. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_metadata.py +0 -0
  220. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_opc.py +0 -0
  221. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_package.py +0 -0
  222. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_refresh.py +0 -0
  223. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_section.py +0 -0
  224. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_sheets.py +0 -0
  225. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/workbook.py +0 -0
  226. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/__init__.py +0 -0
  227. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_docx.py +0 -0
  228. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_docx_text.py +0 -0
  229. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_drawingml.py +0 -0
  230. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_pptx.py +0 -0
  231. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_values.py +0 -0
  232. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_xlsx.py +0 -0
  233. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/vba.py +0 -0
  234. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/vba_pcode.py +0 -0
  235. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/word.py +0 -0
  236. {pyopenvba-6.4.0 → pyopenvba-6.5.0}/tests/fuzz_corpus/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyOpenVBA
3
- Version: 6.4.0
3
+ Version: 6.5.0
4
4
  Summary: Read and write VBA macros and Excel Power Query in pure Python, no dependencies.
5
5
  Project-URL: Homepage, https://github.com/WilliamSmithEdward/pyOpenVBA
6
6
  Project-URL: Repository, https://github.com/WilliamSmithEdward/pyOpenVBA
@@ -993,6 +993,57 @@ decompress and rewrite every retained part and detect such damage. This does
993
993
  not repair damaged parts. Unsupported packages fall back to a full rebuild;
994
994
  legacy binary formats ignore this flag.
995
995
 
996
+ ### File integrity checks and conservative repair
997
+
998
+ Check a file explicitly before or after editing, independently of the fast
999
+ save path:
1000
+
1001
+ ```python
1002
+ from pyopenvba import check_file, repair_file, FileRepairError
1003
+
1004
+ report = check_file("book.xlsm")
1005
+ for issue in report.issues:
1006
+ print(issue.severity, issue.code, issue.location, issue.message)
1007
+
1008
+ if report.ok:
1009
+ print("All supported integrity checks passed")
1010
+ elif report.complete and report.errors and all(i.repairable for i in report.errors):
1011
+ try:
1012
+ result = repair_file("book.xlsm", output="book.repaired.xlsm")
1013
+ print(result.changes, result.after.ok)
1014
+ except FileRepairError as exc:
1015
+ print(exc.report.issues)
1016
+ ```
1017
+
1018
+ `check_file()` returns a `FileCheckReport` containing `errors`, `warnings`,
1019
+ `checked_parts`, and `complete`. `ok` requires no errors and complete coverage
1020
+ of the supported checks. ZIP parts are decompressed and CRC checked; local
1021
+ headers, data descriptors, XML, content types, and internal relationships are
1022
+ checked too. CFB directory pointers and stream sizes are checked, and supported
1023
+ VBA projects have their module sources and forms parsed. VBA is never executed.
1024
+ These checks do not prove that Office can open the document, compile its VBA,
1025
+ or verify its digital signatures.
1026
+
1027
+ Repair initially supports **stale local ZIP CRC/size fields only**, when the
1028
+ actual payload passes central-directory verification. It preserves every
1029
+ other byte and checks the candidate before writing a new file. It refuses
1030
+ missing parts, corrupt payloads, ambiguous entries, and incomplete checks;
1031
+ `FileRepairError.report` explains the refusal. An existing output is never
1032
+ overwritten, and a healthy input can be copied without changes.
1033
+
1034
+ The default limits are 256 MiB per uncompressed part and 1 GiB for both input
1035
+ size and the sum of declared uncompressed sizes. Raise `max_part_bytes` and
1036
+ `max_total_bytes` explicitly for trusted larger files. Invalid limits, oversized
1037
+ input, missing paths, and filesystem failures raise exceptions. A skipped part
1038
+ is reported as incomplete. ZIP64 payloads are verified, but local ZIP64 size
1039
+ metadata is currently reported as incomplete. Unknown VBA formats and Access
1040
+ coverage are also explicit warnings: Access checks its catalog and VBA, not
1041
+ every table row or allocation map. Access's existing `compact_and_repair()`
1042
+ returns a rebuilt database and remains a separate operation.
1043
+
1044
+ Encrypted ZIP parts and compression methods other than stored/deflate are
1045
+ reported as incomplete and cannot be repaired by this API.
1046
+
996
1047
  `save()` enforces the following protection and signature gates.
997
1048
 
998
1049
  ### Password-protected projects
@@ -956,6 +956,57 @@ decompress and rewrite every retained part and detect such damage. This does
956
956
  not repair damaged parts. Unsupported packages fall back to a full rebuild;
957
957
  legacy binary formats ignore this flag.
958
958
 
959
+ ### File integrity checks and conservative repair
960
+
961
+ Check a file explicitly before or after editing, independently of the fast
962
+ save path:
963
+
964
+ ```python
965
+ from pyopenvba import check_file, repair_file, FileRepairError
966
+
967
+ report = check_file("book.xlsm")
968
+ for issue in report.issues:
969
+ print(issue.severity, issue.code, issue.location, issue.message)
970
+
971
+ if report.ok:
972
+ print("All supported integrity checks passed")
973
+ elif report.complete and report.errors and all(i.repairable for i in report.errors):
974
+ try:
975
+ result = repair_file("book.xlsm", output="book.repaired.xlsm")
976
+ print(result.changes, result.after.ok)
977
+ except FileRepairError as exc:
978
+ print(exc.report.issues)
979
+ ```
980
+
981
+ `check_file()` returns a `FileCheckReport` containing `errors`, `warnings`,
982
+ `checked_parts`, and `complete`. `ok` requires no errors and complete coverage
983
+ of the supported checks. ZIP parts are decompressed and CRC checked; local
984
+ headers, data descriptors, XML, content types, and internal relationships are
985
+ checked too. CFB directory pointers and stream sizes are checked, and supported
986
+ VBA projects have their module sources and forms parsed. VBA is never executed.
987
+ These checks do not prove that Office can open the document, compile its VBA,
988
+ or verify its digital signatures.
989
+
990
+ Repair initially supports **stale local ZIP CRC/size fields only**, when the
991
+ actual payload passes central-directory verification. It preserves every
992
+ other byte and checks the candidate before writing a new file. It refuses
993
+ missing parts, corrupt payloads, ambiguous entries, and incomplete checks;
994
+ `FileRepairError.report` explains the refusal. An existing output is never
995
+ overwritten, and a healthy input can be copied without changes.
996
+
997
+ The default limits are 256 MiB per uncompressed part and 1 GiB for both input
998
+ size and the sum of declared uncompressed sizes. Raise `max_part_bytes` and
999
+ `max_total_bytes` explicitly for trusted larger files. Invalid limits, oversized
1000
+ input, missing paths, and filesystem failures raise exceptions. A skipped part
1001
+ is reported as incomplete. ZIP64 payloads are verified, but local ZIP64 size
1002
+ metadata is currently reported as incomplete. Unknown VBA formats and Access
1003
+ coverage are also explicit warnings: Access checks its catalog and VBA, not
1004
+ every table row or allocation map. Access's existing `compact_and_repair()`
1005
+ returns a rebuilt database and remains a separate operation.
1006
+
1007
+ Encrypted ZIP parts and compression methods other than stored/deflate are
1008
+ reported as incomplete and cannot be repaired by this API.
1009
+
959
1010
  `save()` enforces the following protection and signature gates.
960
1011
 
961
1012
  ### Password-protected projects
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyOpenVBA"
7
- version = "6.4.0"
7
+ version = "6.5.0"
8
8
  description = "Read and write VBA macros and Excel Power Query in pure Python, no dependencies."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -81,6 +81,9 @@ from pyopenvba.exceptions import (
81
81
  VBAProjectError,
82
82
  )
83
83
  from pyopenvba.forms import FormControl, Size, VBAForm
84
+ from pyopenvba.integrity import (
85
+ FileCheckReport, FileIssue, FileRepairError, FileRepairResult, check_file, repair_file,
86
+ )
84
87
  from pyopenvba.powerpoint import PowerPointFile
85
88
  from pyopenvba.powerquery import PowerQuery, PowerQueryWorkbook, QueryGroup, RefreshSettings
86
89
  from pyopenvba.powerquery import pull_queries as _pull_queries
@@ -228,6 +231,12 @@ def push_access(
228
231
 
229
232
 
230
233
  __all__ = [
234
+ "FileCheckReport",
235
+ "FileIssue",
236
+ "FileRepairError",
237
+ "FileRepairResult",
238
+ "check_file",
239
+ "repair_file",
231
240
  "AccessDatabase",
232
241
  "AccessReader",
233
242
  "CFBError",
@@ -293,4 +302,4 @@ def push_power_query(
293
302
  """
294
303
  return _push_queries(src_dir, workbook, out=out, encoding=encoding, remove_missing=remove_missing)
295
304
 
296
- __version__ = "6.4.0"
305
+ __version__ = "6.5.0"
@@ -0,0 +1,506 @@
1
+ """Explicit file diagnostics and conservative, content-preserving ZIP repair.
2
+
3
+ These checks do not execute VBA, verify signatures, or prove that Office can
4
+ open a document. Access diagnostics cover the catalog and VBA, not every row
5
+ or allocation map; use AccessDatabase.compact_and_repair for compaction.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ # The checker deliberately inspects CFB directory sizes and host format
11
+ # metadata, which are internal to the parsers it validates.
12
+ # pyright: reportPrivateUsage=false
13
+
14
+ import io
15
+ import posixpath
16
+ import struct
17
+ import zipfile
18
+ import zlib
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import Literal, NoReturn
22
+ from urllib.parse import unquote, urlsplit
23
+ from xml.parsers import expat
24
+
25
+ from pyopenvba.cfb import CFB
26
+ from pyopenvba.exceptions import NoVBAProjectError, PyOpenVBAError
27
+
28
+ _REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships}"
29
+ _CT_NS = "http://schemas.openxmlformats.org/package/2006/content-types}"
30
+ _DEFAULT_PART = 256 * 1024 * 1024
31
+ _DEFAULT_TOTAL = 1024 * 1024 * 1024
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class FileIssue:
36
+ """A stable diagnostic code, severity, location, and explanation."""
37
+
38
+ code: str
39
+ severity: Literal["error", "warning"]
40
+ location: str
41
+ message: str
42
+ repairable: bool = False
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class FileCheckReport:
47
+ """Results for the on-disk snapshot, not unsaved in-memory edits.
48
+
49
+ ``ok`` requires no errors and complete coverage of the supported checks.
50
+ Warnings explain unsupported coverage. This is not Office certification.
51
+ """
52
+
53
+ path: Path
54
+ issues: tuple[FileIssue, ...]
55
+ checked_parts: int
56
+ complete: bool
57
+
58
+ @property
59
+ def errors(self) -> tuple[FileIssue, ...]:
60
+ return tuple(issue for issue in self.issues if issue.severity == "error")
61
+
62
+ @property
63
+ def warnings(self) -> tuple[FileIssue, ...]:
64
+ return tuple(issue for issue in self.issues if issue.severity == "warning")
65
+
66
+ @property
67
+ def ok(self) -> bool:
68
+ return self.complete and not self.errors
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class FileRepairResult:
73
+ """The original findings, changes actually made, and verified output."""
74
+
75
+ output: Path
76
+ before: FileCheckReport
77
+ after: FileCheckReport
78
+ changes: tuple[FileIssue, ...]
79
+
80
+
81
+ class FileRepairError(PyOpenVBAError):
82
+ """Repair refused because content or unsupported structures need guessing."""
83
+
84
+ def __init__(self, message: str, report: FileCheckReport) -> None:
85
+ super().__init__(message)
86
+ self.report = report
87
+
88
+
89
+ def _xml(data: bytes) -> tuple[str, list[tuple[str, dict[str, str]]]]:
90
+ """Parse well-formed XML without DTDs or entity expansion; retain children."""
91
+ parser = expat.ParserCreate(namespace_separator="}")
92
+ root = ""
93
+ depth = 0
94
+ children: list[tuple[str, dict[str, str]]] = []
95
+
96
+ def start(name: str, attrs: dict[str, str]) -> None:
97
+ nonlocal root, depth
98
+ if depth == 0:
99
+ root = name
100
+ elif depth == 1:
101
+ children.append((name, attrs))
102
+ depth += 1
103
+
104
+ def end(_name: str) -> None:
105
+ nonlocal depth
106
+ depth -= 1
107
+
108
+ def refuse_dtd(*_args: object) -> NoReturn:
109
+ raise ValueError("DTDs and declared entities are not supported")
110
+
111
+ parser.StartElementHandler = start
112
+ parser.EndElementHandler = end
113
+ parser.StartDoctypeDeclHandler = refuse_dtd
114
+ parser.EntityDeclHandler = refuse_dtd
115
+ parser.ExternalEntityRefHandler = refuse_dtd
116
+ parser.SetParamEntityParsing(expat.XML_PARAM_ENTITY_PARSING_NEVER)
117
+ parser.Parse(data, True)
118
+ return root, children
119
+
120
+
121
+ def _verify_deflate(raw: bytes, info: zipfile.ZipInfo, limit: int) -> None:
122
+ """Do not trust file_size: ZipExtFile can trim output to that size."""
123
+ name_size, extra_size = struct.unpack_from("<HH", raw, info.header_offset + 26)
124
+ start = info.header_offset + 30 + name_size + extra_size
125
+ end = start + info.compress_size
126
+ if end > len(raw):
127
+ raise ValueError("Truncated compressed payload")
128
+ decoder = zlib.decompressobj(-15)
129
+ size = 0
130
+ crc = 0
131
+ for offset in range(start, end, 1024 * 1024):
132
+ chunk = raw[offset:min(end, offset + 1024 * 1024)]
133
+ while chunk:
134
+ output = decoder.decompress(chunk, min(1024 * 1024, limit - size + 1))
135
+ size += len(output)
136
+ if size > limit:
137
+ raise ValueError("Actual decompressed payload exceeds byte limit")
138
+ crc = zlib.crc32(output, crc)
139
+ chunk = decoder.unconsumed_tail
140
+ if decoder.unused_data:
141
+ raise ValueError("Trailing bytes within compressed payload")
142
+ if not decoder.eof or size != info.file_size or crc != info.CRC:
143
+ raise ValueError("Actual deflate payload size or CRC disagrees with central directory")
144
+
145
+
146
+ def _cfb_check(raw: bytes) -> None:
147
+ cfb = CFB.from_bytes(raw)
148
+ entries = cfb._directory
149
+ if not entries or entries[0].obj_type != 5 or sum(entry.obj_type == 5 for entry in entries) != 1:
150
+ raise ValueError("CFB directory must have one root entry at index zero")
151
+ active = {index for index, entry in enumerate(entries) if entry.obj_type in {1, 2, 5}}
152
+ states: dict[int, int] = {}
153
+ # The reader tolerates broken directory pointers; diagnostics must not.
154
+ for first in active:
155
+ stack = [(first, False)]
156
+ while stack:
157
+ index, leaving = stack.pop()
158
+ if leaving:
159
+ states[index] = 2
160
+ continue
161
+ if states.get(index) == 1:
162
+ raise ValueError("Cycle in CFB directory pointers")
163
+ if states.get(index) == 2:
164
+ continue
165
+ states[index] = 1
166
+ stack.append((index, True))
167
+ entry = entries[index]
168
+ for pointer in (entry.left_sibling_id, entry.right_sibling_id, entry.child_id):
169
+ if pointer == 0xFFFFFFFF:
170
+ continue
171
+ if pointer not in active:
172
+ raise ValueError("CFB directory pointer references a missing entry")
173
+ stack.append((pointer, False))
174
+ # Include orphan directory entries, and compare against declared sizes:
175
+ # the normal reader intentionally returns the bytes it can retrieve.
176
+ for entry in cfb._directory:
177
+ if entry.obj_type == 2 and len(cfb._read_stream(entry)) != entry.size:
178
+ raise ValueError(f"Truncated CFB stream: {entry.name}")
179
+ pending: list[tuple[str, ...]] = [()]
180
+ seen: set[tuple[str, ...]] = set()
181
+ # Bound traversal even if a malformed directory cycles through storages.
182
+ while pending:
183
+ path = pending.pop()
184
+ if path in seen or len(path) > 128 or len(seen) > 100_000:
185
+ raise ValueError("CFB directory traversal is cyclic or exceeds limits")
186
+ seen.add(path)
187
+ streams = cfb.list_streams_at(path)
188
+ storages = cfb.list_storages_at(path)
189
+ names = [name.casefold() for name in streams + storages]
190
+ if len(names) != len(set(names)):
191
+ raise ValueError("Duplicate CFB directory names within one storage")
192
+ for name in streams:
193
+ cfb.get_stream_at(path, name)
194
+ pending.extend((*path, name) for name in storages)
195
+
196
+
197
+ def _host_check(path: Path, raw: bytes, issues: list[FileIssue]) -> bool:
198
+ # Imports here keep the diagnostics module independent of the public facade.
199
+ from pyopenvba.excel import ExcelFile
200
+ from pyopenvba.powerpoint import PowerPointFile
201
+ from pyopenvba.word import WordFile
202
+ from pyopenvba.vba import parse_vba_project
203
+
204
+ plain_formats = {ExcelFile: {".xlsx", ".xltx"}, WordFile: {".docx", ".dotx"},
205
+ PowerPointFile: {".pptx", ".potx", ".ppsx"}}
206
+ for host_type in (ExcelFile, WordFile, PowerPointFile):
207
+ if path.suffix.lower() in host_type._zip_formats | host_type._cfb_formats | plain_formats[host_type]:
208
+ # Use the immutable snapshot, without reopening a potentially changed file.
209
+ if raw.startswith(b"PK"):
210
+ with zipfile.ZipFile(io.BytesIO(raw)) as archive:
211
+ if host_type._vba_entry not in archive.namelist():
212
+ return True
213
+ project_raw = archive.read(host_type._vba_entry)
214
+ else:
215
+ project_raw = raw
216
+ if host_type is PowerPointFile:
217
+ from pyopenvba._ppt_container import extract_vba_storage
218
+ try:
219
+ project_raw = extract_vba_storage(CFB.from_bytes(raw))
220
+ except NoVBAProjectError:
221
+ return True
222
+ cfb = CFB.from_bytes(project_raw)
223
+ if not raw.startswith(b"PK") and host_type._project_storage is not None:
224
+ if host_type._project_storage.casefold() not in {name.casefold() for name in cfb.list_storages_at()}:
225
+ return True
226
+ _cfb_check(project_raw)
227
+ project = parse_vba_project(cfb)
228
+ for module in project.modules:
229
+ # Source is lazy: force every compressed chunk to be decoded.
230
+ _ = module.source
231
+ from pyopenvba.forms import read_forms
232
+ root = () if raw.startswith(b"PK") or host_type._project_storage is None else (host_type._project_storage,)
233
+ read_forms(cfb, code_page=project.code_page, root=root)
234
+ for message in project.validate(cfb):
235
+ issues.append(FileIssue("vba.structure", "error", path.name, message))
236
+ return True
237
+ return False
238
+
239
+
240
+ def _package_xml(parts: dict[str, bytes], names: set[str], issues: list[FileIssue]) -> None:
241
+ parsed: dict[str, tuple[str, list[tuple[str, dict[str, str]]]]] = {}
242
+ for name, data in parts.items():
243
+ if name.lower().endswith((".xml", ".rels")):
244
+ try:
245
+ parsed[name] = _xml(data)
246
+ except (expat.ExpatError, ValueError) as exc:
247
+ issues.append(FileIssue("opc.xml", "error", name, str(exc)))
248
+ if "[Content_Types].xml" not in names:
249
+ issues.append(FileIssue("opc.content_types", "error", "[Content_Types].xml", "Missing content types part"))
250
+ if "_rels/.rels" not in names:
251
+ issues.append(FileIssue("opc.relationships", "error", "_rels/.rels", "Missing package relationships part"))
252
+ for name, (root, children) in parsed.items():
253
+ if name.endswith(".rels"):
254
+ if root != _REL_NS + "Relationships":
255
+ issues.append(FileIssue("opc.relationships", "error", name, "Invalid relationships root"))
256
+ continue
257
+ if name == "_rels/.rels":
258
+ folder = ""
259
+ main = [attrs for tag, attrs in children if tag == _REL_NS + "Relationship" and attrs.get("Type") in {
260
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
261
+ "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument",
262
+ }]
263
+ if len(main) != 1 or main[0].get("TargetMode", "Internal") != "Internal":
264
+ issues.append(FileIssue("opc.main_relationship", "error", name, "Expected one internal Office document relationship"))
265
+ else:
266
+ parent, leaf = posixpath.split(name)
267
+ if posixpath.basename(parent) != "_rels":
268
+ issues.append(FileIssue("opc.relationships", "error", name, "Invalid relationships part path"))
269
+ continue
270
+ folder = posixpath.dirname(parent)
271
+ source = posixpath.join(folder, leaf[:-5])
272
+ if source not in names:
273
+ issues.append(FileIssue("opc.relationship_source", "error", name, f"Missing source part: {source}"))
274
+ ids: set[str] = set()
275
+ for tag, attrs in children:
276
+ if tag != _REL_NS + "Relationship":
277
+ issues.append(FileIssue("opc.relationships", "error", name, "Unexpected relationship element"))
278
+ continue
279
+ identity = attrs.get("Id", "")
280
+ target = attrs.get("Target", "")
281
+ if not identity or identity in ids or not target or not attrs.get("Type"):
282
+ issues.append(FileIssue("opc.relationships", "error", name, "Missing attributes or duplicate relationship ID"))
283
+ ids.add(identity)
284
+ if attrs.get("TargetMode") == "External":
285
+ continue
286
+ if attrs.get("TargetMode", "Internal") != "Internal":
287
+ issues.append(FileIssue("opc.relationships", "error", name, "Invalid TargetMode"))
288
+ uri = urlsplit(target)
289
+ resolved = posixpath.normpath(posixpath.join(folder, unquote(uri.path)))
290
+ if uri.path.startswith("/"):
291
+ resolved = posixpath.normpath(unquote(uri.path).lstrip("/"))
292
+ if uri.scheme or uri.netloc or resolved.startswith("../") or resolved not in names:
293
+ issues.append(FileIssue("opc.relationship_target", "error", name, f"Missing or invalid internal target: {target}"))
294
+ elif name == "[Content_Types].xml":
295
+ if root != _CT_NS + "Types":
296
+ issues.append(FileIssue("opc.content_types", "error", name, "Invalid content types root"))
297
+ continue
298
+ defaults: set[str] = set()
299
+ overrides: set[str] = set()
300
+ for tag, attrs in children:
301
+ if tag == _CT_NS + "Default":
302
+ key = attrs.get("Extension", "").lower()
303
+ seen = defaults
304
+ elif tag == _CT_NS + "Override":
305
+ key = unquote(attrs.get("PartName", "")).lstrip("/")
306
+ seen = overrides
307
+ if key not in names:
308
+ issues.append(FileIssue("opc.content_types", "error", name, f"Override refers to missing part: {key}"))
309
+ else:
310
+ issues.append(FileIssue("opc.content_types", "error", name, "Unexpected content type element"))
311
+ continue
312
+ if not key or key in seen or not attrs.get("ContentType"):
313
+ issues.append(FileIssue("opc.content_types", "error", name, "Missing attributes or duplicate content type"))
314
+ seen.add(key)
315
+ for part in names - {"[Content_Types].xml"}:
316
+ if not part.endswith("/") and part not in overrides and part.rsplit(".", 1)[-1].lower() not in defaults:
317
+ issues.append(FileIssue("opc.content_type_missing", "error", part, "No content type declaration"))
318
+
319
+
320
+ def _check(raw: bytes, path: Path, max_part_bytes: int, max_total_bytes: int) -> FileCheckReport:
321
+ issues: list[FileIssue] = []
322
+ complete = True
323
+ checked = 0
324
+ try:
325
+ if raw.startswith(b"PK"):
326
+ parts: dict[str, bytes] = {}
327
+ total = 0
328
+ with zipfile.ZipFile(io.BytesIO(raw)) as archive:
329
+ infos = archive.infolist()
330
+ names = {info.filename for info in infos}
331
+ if len(names) != len(infos):
332
+ issues.append(FileIssue("zip.duplicate", "error", path.name, "Duplicate ZIP part names"))
333
+ for info in infos:
334
+ name = info.filename
335
+ if (info.orig_filename != name or "\\" in name or name.startswith("/")
336
+ or posixpath.normpath(name.rstrip("/")) != name.rstrip("/")):
337
+ issues.append(FileIssue("opc.part_name", "error", name, "Ambiguous or invalid package part name"))
338
+ if info.compress_type not in {zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED} or info.flag_bits & 1:
339
+ issues.append(FileIssue("check.compression", "warning", name, "Encrypted or unsupported compression; payload not checked"))
340
+ complete = False
341
+ continue
342
+ if info.file_size > max_part_bytes or total + info.file_size > max_total_bytes:
343
+ issues.append(FileIssue("check.limit", "warning", name, "Decompression byte limit exceeded"))
344
+ complete = False
345
+ continue
346
+ total += info.file_size
347
+ try:
348
+ # Read in bounded chunks; do not trust declared uncompressed sizes.
349
+ chunks: list[bytes] = []
350
+ count = 0
351
+ with archive.open(info) as stream:
352
+ while True:
353
+ chunk = stream.read(min(1024 * 1024, max_part_bytes - count + 1))
354
+ if not chunk:
355
+ break
356
+ count += len(chunk)
357
+ if count > max_part_bytes:
358
+ raise ValueError("Decompressed part exceeds byte limit")
359
+ chunks.append(chunk)
360
+ data = b"".join(chunks)
361
+ if info.compress_type == zipfile.ZIP_DEFLATED:
362
+ _verify_deflate(raw, info, max_part_bytes)
363
+ elif info.compress_type == zipfile.ZIP_STORED and info.compress_size != info.file_size:
364
+ raise ValueError("Stored payload compressed and uncompressed sizes disagree")
365
+ checked += 1
366
+ # Keep only XML for cross-part checks; binary payloads need no tree.
367
+ if name.lower().endswith((".xml", ".rels")):
368
+ parts[name] = data
369
+ if data.startswith(b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"):
370
+ _cfb_check(data)
371
+ header = raw[info.header_offset:info.header_offset + 30]
372
+ if len(header) != 30 or header[:4] != b"PK\x03\x04":
373
+ raise ValueError("Missing local ZIP header")
374
+ flags, method = struct.unpack_from("<HH", header, 6)
375
+ crc, compressed, size = struct.unpack_from("<III", header, 14)
376
+ name_size, extra_size = struct.unpack_from("<HH", header, 26)
377
+ extra_start = info.header_offset + 30 + name_size
378
+ extra = raw[extra_start:extra_start + extra_size]
379
+ extra_offset = 0
380
+ zip64 = False
381
+ while extra_offset < len(extra):
382
+ if extra_offset + 4 > len(extra):
383
+ raise ValueError("Truncated local ZIP extra-field header")
384
+ kind, length = struct.unpack_from("<HH", extra, extra_offset)
385
+ extra_offset += 4 + length
386
+ if extra_offset > len(extra):
387
+ raise ValueError("Truncated local ZIP extra-field payload")
388
+ zip64 |= kind == 1
389
+ if flags != info.flag_bits or method != info.compress_type:
390
+ issues.append(FileIssue("zip.local_header", "error", name, "Local flags or compression method disagree with central directory"))
391
+ if flags & 8:
392
+ if crc not in {0, info.CRC} or compressed not in {0, 0xFFFFFFFF, info.compress_size} or size not in {0, 0xFFFFFFFF, info.file_size}:
393
+ issues.append(FileIssue("zip.local_header", "error", name, "Local data-descriptor metadata disagrees with central directory"))
394
+ offset = info.header_offset + 30 + name_size + extra_size + info.compress_size
395
+ expected = struct.pack("<III", info.CRC, info.compress_size, info.file_size) if max(info.compress_size, info.file_size) <= 0xFFFFFFFF else b""
396
+ if zip64 or compressed == 0xFFFFFFFF or size == 0xFFFFFFFF or not expected:
397
+ issues.append(FileIssue("check.zip64", "warning", name, "ZIP64 payload checked; data descriptor not checked"))
398
+ complete = False
399
+ elif raw[offset:offset + 12] != expected and raw[offset:offset + 16] != b"PK\x07\x08" + expected:
400
+ issues.append(FileIssue("zip.descriptor", "error", name, "Missing or inconsistent ZIP data descriptor"))
401
+ else:
402
+ if zip64 or compressed == 0xFFFFFFFF or size == 0xFFFFFFFF:
403
+ issues.append(FileIssue("check.zip64", "warning", name, "ZIP64 payload checked; local size metadata not checked"))
404
+ complete = False
405
+ elif (crc, compressed, size) != (info.CRC, info.compress_size, info.file_size):
406
+ issues.append(FileIssue("zip.local_metadata", "error", name, "Local CRC or sizes disagree with verified central directory", True))
407
+ except Exception as exc:
408
+ issues.append(FileIssue("zip.part", "error", name, str(exc)))
409
+ _package_xml(parts, names, issues)
410
+ if complete:
411
+ if not _host_check(path, raw, issues):
412
+ issues.append(FileIssue("check.vba_scope", "warning", path.name, "VBA validation is unavailable for this extension"))
413
+ complete = False
414
+ elif raw.startswith(b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"):
415
+ _cfb_check(raw)
416
+ checked = 1
417
+ if not _host_check(path, raw, issues):
418
+ issues.append(FileIssue("check.vba_scope", "warning", path.name, "CFB streams checked; VBA validation is unavailable for this extension"))
419
+ complete = False
420
+ elif path.suffix.lower() in {".accdb", ".mdb"}:
421
+ issues.append(FileIssue("check.access_scope", "warning", path.name, "Access allocation maps and table rows are not covered; use the Access APIs for database inspection and compaction"))
422
+ complete = False
423
+ from pyopenvba.access import AccessDatabase
424
+ with AccessDatabase(raw) as database:
425
+ database.table_names(include_system=True)
426
+ if database.has_vba_project():
427
+ for module in database.vba_project().modules:
428
+ _ = module.source
429
+ checked = 1
430
+ else:
431
+ issues.append(FileIssue("file.format", "error", path.name, "Not a supported Office ZIP, CFB, or Access container"))
432
+ complete = False
433
+ except Exception as exc:
434
+ issues.append(FileIssue("file.parse", "error", path.name, str(exc)))
435
+ complete = False
436
+ return FileCheckReport(path, tuple(issues), checked, complete)
437
+
438
+
439
+ def _snapshot(path: Path, max_part_bytes: int, max_total_bytes: int) -> bytes:
440
+ if max_part_bytes <= 0 or max_total_bytes <= 0:
441
+ raise ValueError("Byte limits must be positive")
442
+ with path.open("rb") as stream:
443
+ raw = stream.read(max_total_bytes + 1)
444
+ if len(raw) > max_total_bytes:
445
+ raise ValueError("Input file exceeds max_total_bytes")
446
+ return raw
447
+
448
+
449
+ def check_file(path: str | Path, *, max_part_bytes: int = _DEFAULT_PART,
450
+ max_total_bytes: int = _DEFAULT_TOTAL) -> FileCheckReport:
451
+ """Check a disk snapshot without modifying it or executing VBA.
452
+
453
+ ZIP parts are decompressed and CRC checked, XML rejects DTDs, internal
454
+ relationships and content types are checked, and supported VBA projects
455
+ are parsed and validated. Legacy CFB streams are read. Access coverage
456
+ is partial and explicitly reported. Missing files and invalid limits raise;
457
+ malformed contents are returned as diagnostics. Limits can be raised for
458
+ trusted large files; they bound input and uncompressed payload sizes.
459
+ """
460
+ source = Path(path)
461
+ return _check(_snapshot(source, max_part_bytes, max_total_bytes), source,
462
+ max_part_bytes, max_total_bytes)
463
+
464
+
465
+ def repair_file(path: str | Path, *, output: str | Path,
466
+ max_part_bytes: int = _DEFAULT_PART,
467
+ max_total_bytes: int = _DEFAULT_TOTAL) -> FileRepairResult:
468
+ """Repair stale ZIP local-header CRC/size fields into a new file.
469
+
470
+ Payloads must pass central-directory CRC verification and all supported
471
+ checks. No parts, VBA, signatures, or compressed bytes are changed. Other
472
+ damage or incomplete checking raises FileRepairError with its report.
473
+ The output must not exist (including the original file); it is written
474
+ only after checking the candidate. A healthy file is copied unchanged.
475
+ This does not rebuild databases or recover missing/corrupt content.
476
+ """
477
+ source, target = Path(path), Path(output)
478
+ if source.resolve() == target.resolve():
479
+ raise ValueError("Repair output must differ from the original file")
480
+ if target.exists():
481
+ raise FileExistsError(target)
482
+ raw = _snapshot(source, max_part_bytes, max_total_bytes)
483
+ before = _check(raw, source, max_part_bytes, max_total_bytes)
484
+ if not before.complete or any(not issue.repairable for issue in before.errors):
485
+ raise FileRepairError("File has damage or unsupported checks that cannot be safely repaired", before)
486
+ changes = before.errors
487
+ candidate = bytearray(raw)
488
+ if changes:
489
+ with zipfile.ZipFile(io.BytesIO(raw)) as archive:
490
+ names = {issue.location for issue in changes}
491
+ for info in archive.infolist():
492
+ if info.filename in names:
493
+ struct.pack_into("<III", candidate, info.header_offset + 14,
494
+ info.CRC, info.compress_size, info.file_size)
495
+ after = _check(bytes(candidate), source, max_part_bytes, max_total_bytes)
496
+ if not after.ok:
497
+ raise FileRepairError("Candidate failed verification; no output written", after)
498
+ stream = target.open("xb")
499
+ try:
500
+ with stream:
501
+ stream.write(candidate)
502
+ except BaseException:
503
+ target.unlink(missing_ok=True)
504
+ raise
505
+ after = FileCheckReport(target, after.issues, after.checked_parts, after.complete)
506
+ return FileRepairResult(target, before, after, changes)
File without changes
File without changes