mxrb 0.1.0

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 (237) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.de-DE.md +108 -0
  4. data/README.md +172 -0
  5. data/README.pt-BR.md +109 -0
  6. data/bin/mxrb +1613 -0
  7. data/docs/README.md +10 -0
  8. data/docs/de-DE/README.md +24 -0
  9. data/docs/de-DE/architectural-patterns.md +26 -0
  10. data/docs/de-DE/architectural-standard.md +48 -0
  11. data/docs/de-DE/architecture.md +76 -0
  12. data/docs/de-DE/compiler.md +162 -0
  13. data/docs/de-DE/conventions.md +19 -0
  14. data/docs/de-DE/design-system.md +52 -0
  15. data/docs/de-DE/entity-dsl.md +53 -0
  16. data/docs/de-DE/native-runtime-quality-report.md +74 -0
  17. data/docs/de-DE/oql-sql.md +211 -0
  18. data/docs/de-DE/platform-api-integration.md +36 -0
  19. data/docs/de-DE/platform-operations.md +243 -0
  20. data/docs/de-DE/project-structure.md +32 -0
  21. data/docs/de-DE/ruby-first-roadmap.md +116 -0
  22. data/docs/de-DE/scaffolds.md +59 -0
  23. data/docs/de-DE/semantic-refactoring.md +56 -0
  24. data/docs/de-DE/team-server.md +33 -0
  25. data/docs/de-DE/validation-matrix.md +64 -0
  26. data/docs/de-DE/vetclinic-acceptance.md +24 -0
  27. data/docs/de-DE/writing.md +138 -0
  28. data/docs/en-US/README.md +24 -0
  29. data/docs/en-US/architectural-patterns.md +151 -0
  30. data/docs/en-US/architectural-standard.md +89 -0
  31. data/docs/en-US/architecture.md +170 -0
  32. data/docs/en-US/compiler.md +157 -0
  33. data/docs/en-US/conventions.md +82 -0
  34. data/docs/en-US/design-system.md +163 -0
  35. data/docs/en-US/entity-dsl.md +53 -0
  36. data/docs/en-US/native-runtime-quality-report.md +73 -0
  37. data/docs/en-US/oql-sql.md +201 -0
  38. data/docs/en-US/platform-api-integration.md +36 -0
  39. data/docs/en-US/platform-operations.md +244 -0
  40. data/docs/en-US/project-structure.md +49 -0
  41. data/docs/en-US/ruby-first-roadmap.md +186 -0
  42. data/docs/en-US/scaffolds.md +57 -0
  43. data/docs/en-US/semantic-refactoring.md +198 -0
  44. data/docs/en-US/team-server.md +32 -0
  45. data/docs/en-US/validation-matrix.md +257 -0
  46. data/docs/en-US/vetclinic-acceptance.md +23 -0
  47. data/docs/en-US/writing.md +655 -0
  48. data/docs/pt-BR/README.md +24 -0
  49. data/docs/pt-BR/architectural-patterns.md +25 -0
  50. data/docs/pt-BR/architectural-standard.md +533 -0
  51. data/docs/pt-BR/architecture.md +300 -0
  52. data/docs/pt-BR/compiler.md +162 -0
  53. data/docs/pt-BR/conventions.md +19 -0
  54. data/docs/pt-BR/design-system.md +65 -0
  55. data/docs/pt-BR/entity-dsl.md +116 -0
  56. data/docs/pt-BR/native-runtime-quality-report.md +73 -0
  57. data/docs/pt-BR/oql-sql.md +208 -0
  58. data/docs/pt-BR/platform-api-integration.md +36 -0
  59. data/docs/pt-BR/platform-operations.md +260 -0
  60. data/docs/pt-BR/project-structure.md +32 -0
  61. data/docs/pt-BR/ruby-first-roadmap.md +239 -0
  62. data/docs/pt-BR/scaffolds.md +104 -0
  63. data/docs/pt-BR/semantic-refactoring.md +55 -0
  64. data/docs/pt-BR/team-server.md +59 -0
  65. data/docs/pt-BR/validation-matrix.md +86 -0
  66. data/docs/pt-BR/vetclinic-acceptance.md +50 -0
  67. data/docs/pt-BR/writing.md +177 -0
  68. data/examples/sudoku_evaluation.rb +14 -0
  69. data/examples/sudoku_functional_test.rb +28 -0
  70. data/lib/mxrb/architecture/graph.rb +124 -0
  71. data/lib/mxrb/architecture/validator.rb +98 -0
  72. data/lib/mxrb/benchmark.rb +33 -0
  73. data/lib/mxrb/compare.rb +390 -0
  74. data/lib/mxrb/compiler/adapter.rb +96 -0
  75. data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
  76. data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
  77. data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
  78. data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
  79. data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
  80. data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
  81. data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
  82. data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
  83. data/lib/mxrb/compiler/constants_materializer.rb +111 -0
  84. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
  85. data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
  86. data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
  87. data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
  88. data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
  89. data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
  90. data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
  91. data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
  92. data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
  93. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
  94. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
  95. data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
  96. data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
  97. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
  98. data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
  99. data/lib/mxrb/compiler/mda.rb +99 -0
  100. data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
  101. data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
  102. data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
  103. data/lib/mxrb/compiler/model_package.rb +116 -0
  104. data/lib/mxrb/compiler/model_values.rb +67 -0
  105. data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
  106. data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
  107. data/lib/mxrb/compiler/packager.rb +100 -0
  108. data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
  109. data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
  110. data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
  111. data/lib/mxrb/compiler/portable_packager.rb +356 -0
  112. data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
  113. data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
  114. data/lib/mxrb/compiler/project_materializer.rb +139 -0
  115. data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
  116. data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
  117. data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
  118. data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
  119. data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
  120. data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
  121. data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
  122. data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
  123. data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
  124. data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
  125. data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
  126. data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
  127. data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
  128. data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
  129. data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
  130. data/lib/mxrb/compiler/security_materializer.rb +104 -0
  131. data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
  132. data/lib/mxrb/compiler/settings_materializer.rb +25 -0
  133. data/lib/mxrb/compiler/source_model.rb +128 -0
  134. data/lib/mxrb/compiler/system_model_seed.rb +88 -0
  135. data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
  136. data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
  137. data/lib/mxrb/compiler/translation_materializer.rb +95 -0
  138. data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
  139. data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
  140. data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
  141. data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
  142. data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
  143. data/lib/mxrb/doctor.rb +116 -0
  144. data/lib/mxrb/dsl/builder.rb +1636 -0
  145. data/lib/mxrb/errors.rb +18 -0
  146. data/lib/mxrb/evaluation.rb +131 -0
  147. data/lib/mxrb/exporter.rb +1716 -0
  148. data/lib/mxrb/frontend/migrator.rb +775 -0
  149. data/lib/mxrb/functional.rb +307 -0
  150. data/lib/mxrb/github/annotator.rb +161 -0
  151. data/lib/mxrb/initializer.rb +257 -0
  152. data/lib/mxrb/integrity/validator.rb +136 -0
  153. data/lib/mxrb/io/bson_codec.rb +145 -0
  154. data/lib/mxrb/io/mpr_file.rb +558 -0
  155. data/lib/mxrb/io/mxunit_codec.rb +42 -0
  156. data/lib/mxrb/marketplace.rb +380 -0
  157. data/lib/mxrb/model/association.rb +98 -0
  158. data/lib/mxrb/model/attribute.rb +100 -0
  159. data/lib/mxrb/model/connector.rb +17 -0
  160. data/lib/mxrb/model/design_materializer.rb +133 -0
  161. data/lib/mxrb/model/design_migration.rb +126 -0
  162. data/lib/mxrb/model/design_system.rb +111 -0
  163. data/lib/mxrb/model/domain_model.rb +51 -0
  164. data/lib/mxrb/model/entity.rb +180 -0
  165. data/lib/mxrb/model/menu.rb +38 -0
  166. data/lib/mxrb/model/microflow.rb +100 -0
  167. data/lib/mxrb/model/module.rb +142 -0
  168. data/lib/mxrb/model/navigation.rb +127 -0
  169. data/lib/mxrb/model/page.rb +410 -0
  170. data/lib/mxrb/model/project.rb +187 -0
  171. data/lib/mxrb/model/unit.rb +48 -0
  172. data/lib/mxrb/module_initializer.rb +75 -0
  173. data/lib/mxrb/official_marketplace/content_api.rb +292 -0
  174. data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
  175. data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
  176. data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
  177. data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
  178. data/lib/mxrb/official_marketplace.rb +720 -0
  179. data/lib/mxrb/oql/analyzer.rb +155 -0
  180. data/lib/mxrb/oql/index_advisor.rb +85 -0
  181. data/lib/mxrb/oql/plan_analyzer.rb +185 -0
  182. data/lib/mxrb/oql/server.rb +132 -0
  183. data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
  184. data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
  185. data/lib/mxrb/oql/workload_analyzer.rb +175 -0
  186. data/lib/mxrb/oql/workload_baseline.rb +60 -0
  187. data/lib/mxrb/oql.rb +424 -0
  188. data/lib/mxrb/project_lifecycle.rb +76 -0
  189. data/lib/mxrb/protocols/plan.rb +86 -0
  190. data/lib/mxrb/protocols.rb +123 -0
  191. data/lib/mxrb/runtime/database_workspace.rb +677 -0
  192. data/lib/mxrb/runtime/docker_executor.rb +41 -0
  193. data/lib/mxrb/runtime/docker_workspace.rb +29 -0
  194. data/lib/mxrb/runtime/executor.rb +169 -0
  195. data/lib/mxrb/runtime/java_locator.rb +26 -0
  196. data/lib/mxrb/runtime/native.rb +474 -0
  197. data/lib/mxrb/runtime/sql_server_database.rb +146 -0
  198. data/lib/mxrb/runtime/toolchain.rb +82 -0
  199. data/lib/mxrb/scaffold/cli.rb +113 -0
  200. data/lib/mxrb/scaffold/generator.rb +246 -0
  201. data/lib/mxrb/scaffold/help.rb +80 -0
  202. data/lib/mxrb/scaffold/page_templates.rb +54 -0
  203. data/lib/mxrb/scaffold/recipes.rb +198 -0
  204. data/lib/mxrb/scaffold/registry.rb +82 -0
  205. data/lib/mxrb/scaffold/templates.rb +723 -0
  206. data/lib/mxrb/scaffold/transaction.rb +84 -0
  207. data/lib/mxrb/schema/tables.rb +55 -0
  208. data/lib/mxrb/semantic/analyzer.rb +473 -0
  209. data/lib/mxrb/semantic/batch_plan.rb +68 -0
  210. data/lib/mxrb/semantic/domain_mutator.rb +419 -0
  211. data/lib/mxrb/semantic/embedder.rb +51 -0
  212. data/lib/mxrb/semantic/extractor.rb +290 -0
  213. data/lib/mxrb/semantic/index.rb +623 -0
  214. data/lib/mxrb/semantic/inliner.rb +216 -0
  215. data/lib/mxrb/semantic/mover.rb +153 -0
  216. data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
  217. data/lib/mxrb/semantic/remover.rb +70 -0
  218. data/lib/mxrb/semantic/renamer.rb +211 -0
  219. data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
  220. data/lib/mxrb/semantic/vec_store.rb +75 -0
  221. data/lib/mxrb/team_server.rb +390 -0
  222. data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
  223. data/lib/mxrb/templates/project/11.12.1.json +32 -0
  224. data/lib/mxrb/templates/project/6.10.8.json +50 -0
  225. data/lib/mxrb/templates/project/7.17.0.json +50 -0
  226. data/lib/mxrb/templates/project/7.5.0.json +50 -0
  227. data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
  228. data/lib/mxrb/version.rb +5 -0
  229. data/lib/mxrb/widget_package.rb +338 -0
  230. data/lib/mxrb/widget_synchronizer.rb +31 -0
  231. data/lib/mxrb/writer.rb +3775 -0
  232. data/lib/mxrb.rb +188 -0
  233. data/marketplace/catalog.json +10 -0
  234. data/marketplace/modules/shared-kernel/domain/README.md +4 -0
  235. data/marketplace/modules/shared-kernel/module.rb +5 -0
  236. data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
  237. metadata +447 -0
@@ -0,0 +1,414 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'rexml/document'
7
+ require 'zip'
8
+
9
+ module Mxrb
10
+ module OfficialMarketplace
11
+ WidgetInstallation = Data.define(
12
+ :package, :destination, :sha256, :widget_name, :widget_ids, :archive
13
+ )
14
+
15
+ # Validates the two official Marketplace package envelopes MXRB accepts.
16
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
17
+ class PackageEnvelope
18
+ def self.kind(path)
19
+ Zip::File.open(path) do |archive|
20
+ package = archive.find_entry('package.xml')
21
+ raise MarketplaceError, 'package.xml is missing' unless package
22
+
23
+ document = REXML::Document.new(package.get_input_stream.read)
24
+ names = document.root&.elements&.map(&:name) || []
25
+ return :module if names.include?('modelerProject')
26
+ return :widget if names.include?('clientModule')
27
+
28
+ raise MarketplaceError, 'package.xml does not declare a module or widget'
29
+ end
30
+ rescue Zip::Error, REXML::ParseException => e
31
+ raise MarketplaceError, "invalid Mendix package: #{e.message}"
32
+ end
33
+ end
34
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
35
+
36
+ # Read-only, fail-closed inventory for a clientModule MPK.
37
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
38
+ class WidgetPackageInventory
39
+ attr_reader :name, :version, :widget_ids, :project_filename
40
+
41
+ def initialize(name:, version:, widget_ids:, project_filename:)
42
+ @name = name
43
+ @version = version
44
+ @widget_ids = widget_ids.freeze
45
+ @project_filename = project_filename
46
+ end
47
+
48
+ def self.read(path)
49
+ Zip::File.open(path) do |archive|
50
+ validate_entries!(archive)
51
+ document = package_document(archive)
52
+ clients = elements(document.root, 'clientModule')
53
+ raise MarketplaceError, 'package.xml must declare exactly one clientModule' unless clients.one?
54
+
55
+ client = clients.first
56
+
57
+ name = valid_name(client.attributes['name'])
58
+ version = valid_version(client.attributes['version'])
59
+ widget_paths = elements(client, 'widgetFile').map do |element|
60
+ safe_relative_path(element.attributes['path'])
61
+ end.uniq
62
+ raise MarketplaceError, 'widget package does not declare widgetFiles' if widget_paths.empty?
63
+
64
+ validate_declared_files!(archive, client)
65
+ widget_ids = widget_paths.map { widget_id(archive, _1) }
66
+ filename = project_filename(name, widget_ids)
67
+ new(name:, version:, widget_ids:, project_filename: filename)
68
+ end
69
+ rescue Zip::Error, REXML::ParseException => e
70
+ raise MarketplaceError, "invalid Mendix widget package: #{e.message}"
71
+ end
72
+
73
+ class << self
74
+ private
75
+
76
+ def package_document(archive)
77
+ package = archive.find_entry('package.xml')
78
+ raise MarketplaceError, 'package.xml is missing' unless package
79
+
80
+ REXML::Document.new(package.get_input_stream.read)
81
+ end
82
+
83
+ def validate_entries!(archive)
84
+ normalized = {}
85
+ archive.each do |entry|
86
+ path = safe_relative_path(entry.name, directory: true)
87
+ key = path.downcase
88
+ raise MarketplaceError, "duplicate widget package path #{entry.name.inspect}" if normalized[key]
89
+ raise MarketplaceError, "symbolic link in widget package: #{entry.name}" if entry.symlink?
90
+
91
+ normalized[key] = true
92
+ end
93
+ end
94
+
95
+ def validate_declared_files!(archive, client)
96
+ elements(client, 'file').each do |element|
97
+ relative = safe_relative_path(element.attributes['path'], directory: true)
98
+ present = archive.any? do |entry|
99
+ normalized = entry.name.to_s.tr('\\', '/').delete_suffix('/')
100
+ normalized == relative || normalized.start_with?("#{relative}/")
101
+ end
102
+ raise MarketplaceError, "declared widget file is missing: #{relative}" unless present
103
+ end
104
+ end
105
+
106
+ def widget_id(archive, relative)
107
+ entry = archive.find_entry(relative) || archive.find_entry(relative.tr('/', '\\'))
108
+ raise MarketplaceError, "declared widget definition is missing: #{relative}" unless entry&.file?
109
+
110
+ root = REXML::Document.new(entry.get_input_stream.read).root
111
+ value = root&.name == 'widget' ? root.attributes['id'].to_s : ''
112
+ return value if value.match?(/\A[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+\z/)
113
+
114
+ raise MarketplaceError, "invalid widget ID #{value.inspect} in #{relative}"
115
+ end
116
+
117
+ def project_filename(name, widget_ids)
118
+ namespaces = widget_ids.map { _1.split('.')[0...-2] }.uniq
119
+ raise MarketplaceError, 'widget definitions do not share one package namespace' unless namespaces.one?
120
+
121
+ "#{(namespaces.first + [name]).join('.')}.mpk"
122
+ end
123
+
124
+ def valid_name(value)
125
+ name = value.to_s
126
+ return name if name.match?(/\A[A-Za-z][A-Za-z0-9_]*\z/)
127
+
128
+ raise MarketplaceError, "invalid clientModule name #{name.inspect}"
129
+ end
130
+
131
+ def valid_version(value)
132
+ version = value.to_s
133
+ return version if version.match?(/\A[0-9A-Za-z][0-9A-Za-z.+_-]*\z/)
134
+
135
+ raise MarketplaceError, "invalid clientModule version #{version.inspect}"
136
+ end
137
+
138
+ def safe_relative_path(value, directory: false)
139
+ path = value.to_s.tr('\\', '/').delete_suffix('/')
140
+ parts = path.split('/')
141
+ unsafe = path.empty? || path.start_with?('/') || parts.include?('..') || parts.include?('') ||
142
+ path.include?("\0")
143
+ raise MarketplaceError, "unsafe widget package path #{value.inspect}" if unsafe
144
+ if !directory && File.extname(path).downcase != '.xml'
145
+ raise MarketplaceError, "widget definition is not XML: #{value.inspect}"
146
+ end
147
+
148
+ path
149
+ end
150
+
151
+ def elements(root, name)
152
+ found = []
153
+ root&.each_recursive { found << _1 if _1.is_a?(REXML::Element) && _1.name == name }
154
+ found
155
+ end
156
+ end
157
+ end
158
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
159
+
160
+ # Enumerates widget identities delivered either directly or as declared
161
+ # assets of a modelerProject package such as Data Widgets.
162
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
163
+ class WidgetBundleInventory
164
+ attr_reader :kind, :widget_ids
165
+
166
+ def initialize(kind:, widget_ids:)
167
+ @kind = kind
168
+ @widget_ids = widget_ids.freeze
169
+ end
170
+
171
+ def self.read(path)
172
+ kind = PackageEnvelope.kind(path)
173
+ return new(kind:, widget_ids: WidgetPackageInventory.read(path).widget_ids) if kind == :widget
174
+
175
+ ModulePackageInventory.read(path)
176
+ ids = Dir.mktmpdir('mxrb-widget-bundle-') do |temporary|
177
+ Zip::File.open(path) do |archive|
178
+ reader = ModulePackageReader.new(archive)
179
+ descriptor = reader.descriptor
180
+ descriptor.files.grep(%r{\Awidgets/[^/]+\.mpk\z}i).flat_map do |relative|
181
+ entry = archive.find_entry(relative) || archive.find_entry(relative.tr('/', '\\'))
182
+ destination = File.join(temporary, File.basename(relative))
183
+ File.open(destination, 'wb') { ::IO.copy_stream(entry.get_input_stream, _1) }
184
+ WidgetPackageInventory.read(destination).widget_ids
185
+ end
186
+ end
187
+ end
188
+ new(kind:, widget_ids: ids.uniq.sort)
189
+ rescue Zip::Error, REXML::ParseException => e
190
+ raise MarketplaceError, "invalid Mendix widget bundle: #{e.message}"
191
+ end
192
+ end
193
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
194
+
195
+ # Installs an already authenticated official widget archive as one project-owned MPK.
196
+ # rubocop:disable Metrics
197
+ class WidgetPackageInstaller
198
+ def initialize(target:)
199
+ @target = File.expand_path(target)
200
+ end
201
+
202
+ def install(archive, package)
203
+ validate_target!
204
+ inventory = WidgetPackageInventory.read(archive)
205
+ validate_identity!(inventory, package)
206
+ digest = Digest::SHA256.file(archive).hexdigest
207
+ destination = safe_path(File.join('widgets', inventory.project_filename))
208
+ cache = safe_path(cache_relative(inventory, package))
209
+ lock_path = safe_path(File.join('.mxrb', 'marketplace.lock.json'))
210
+ current = validate_boundaries!(package, destination, cache, lock_path)
211
+ install_transaction(
212
+ archive, package, inventory, digest, destination, cache, lock_path, current
213
+ )
214
+ end
215
+
216
+ private
217
+
218
+ def validate_target!
219
+ raise MarketplaceError, "Marketplace target not found: #{@target}" unless File.directory?(@target)
220
+ raise MarketplaceError, "Marketplace target is a symbolic link: #{@target}" if File.symlink?(@target)
221
+ end
222
+
223
+ def validate_identity!(inventory, package)
224
+ return if inventory.version == package.version.to_s
225
+
226
+ raise MarketplaceError,
227
+ "widget package version #{inventory.version} does not match Marketplace #{package.version}"
228
+ end
229
+
230
+ def validate_boundaries!(package, destination, cache, lock_path)
231
+ ensure_no_symlink_components(destination)
232
+ ensure_no_symlink_components(cache)
233
+ ensure_no_symlink_components(lock_path)
234
+ raise MarketplaceError, "widget destination is a symbolic link: #{destination}" if File.symlink?(destination)
235
+ raise MarketplaceError, "widget destination is a directory: #{destination}" if File.directory?(destination)
236
+
237
+ lock = OfficialMarketplace.lock(@target)
238
+ current = lock.fetch('packages').find do |name, entry|
239
+ name.casecmp(package.name.to_s).zero? ||
240
+ entry['content_id'].to_s == package.content_id.to_s
241
+ end
242
+ validate_current!(current, package, destination)
243
+ validate_asset_owners!(lock, destination) unless current
244
+ unless !File.exist?(cache) || current&.last&.[]('archive') == relative(cache)
245
+ raise MarketplaceError, "widget cache destination already exists: #{cache}"
246
+ end
247
+
248
+ current
249
+ end
250
+
251
+ def validate_current!(current, package, destination)
252
+ return unless current
253
+
254
+ name, entry = current
255
+ raise MarketplaceError, "Marketplace package #{name.inspect} is not a widget" unless entry['kind'] == 'widget'
256
+ unless entry['destination'] == relative(destination)
257
+ raise MarketplaceError, "Marketplace widget #{name.inspect} changed its destination"
258
+ end
259
+ unless OfficialMarketplace.verify_widget(@target, entry)[:valid]
260
+ raise MarketplaceError, "installed Marketplace widget #{name.inspect} failed verification"
261
+ end
262
+ return unless entry['version'].to_s == package.version.to_s
263
+
264
+ raise MarketplaceError, "widget version #{package.version} is already installed"
265
+ end
266
+
267
+ def validate_asset_owners!(lock, destination)
268
+ relative_path = relative(destination)
269
+ owners = lock.fetch('packages').select do |_name, entry|
270
+ Array(entry['files']).include?(relative_path)
271
+ end
272
+ return if owners.empty? || !File.file?(destination)
273
+
274
+ expected = owners.filter_map { |_name, entry| locked_asset_digest(entry, relative_path) }.uniq
275
+ actual = Digest::SHA256.file(destination).hexdigest
276
+ return if expected.include?(actual)
277
+
278
+ raise MarketplaceError, "owned widget asset changed: #{relative_path}"
279
+ end
280
+
281
+ def locked_asset_digest(entry, relative_path)
282
+ return entry['sha256'] if entry['kind'] == 'widget' && entry['destination'] == relative_path
283
+ return unless entry['kind'] == 'module'
284
+
285
+ archive = safe_path(entry.fetch('archive'))
286
+ raise MarketplaceError, "cached Marketplace package is missing: #{archive}" unless File.file?(archive)
287
+
288
+ ModulePackageInventory.read(archive).files[relative_path]
289
+ end
290
+
291
+ def install_transaction(archive, package, inventory, digest, destination, cache, lock_path, current)
292
+ original = current&.last&.[]('asset_original')
293
+ backup = original ? safe_path(original) : backup_path(package, destination)
294
+ old_cache = safe_path(current.last.fetch('archive')) if current
295
+ paths = [destination, cache, lock_path, backup, old_cache].compact.uniq
296
+ with_rollback(paths) do
297
+ preserve_original(destination, backup) unless original
298
+ atomic_copy(archive, destination)
299
+ atomic_copy(archive, cache)
300
+ verify_copy!(destination, digest)
301
+ verify_copy!(cache, digest)
302
+ write_lock(
303
+ lock_path, package, inventory, digest, destination, cache,
304
+ asset_original: File.file?(backup) ? relative(backup) : nil,
305
+ replaced_name: current&.first
306
+ )
307
+ FileUtils.rm_f(old_cache) if old_cache && old_cache != cache
308
+ end
309
+ WidgetInstallation.new(package, destination, digest, inventory.name, inventory.widget_ids, cache)
310
+ end
311
+
312
+ def preserve_original(destination, backup)
313
+ return unless File.file?(destination)
314
+
315
+ FileUtils.mkdir_p(File.dirname(backup))
316
+ FileUtils.cp(destination, backup)
317
+ end
318
+
319
+ def backup_path(package, destination)
320
+ owner = "Widget-#{package.content_id || package.name.to_s.gsub(/[^A-Za-z0-9_.-]/, '_')}"
321
+ safe_path(File.join('.mxrb', 'marketplace-originals', owner, relative(destination)))
322
+ end
323
+
324
+ def atomic_copy(source, destination)
325
+ temporary = "#{destination}.tmp-#{Process.pid}"
326
+ FileUtils.mkdir_p(File.dirname(destination))
327
+ FileUtils.cp(source, temporary)
328
+ File.rename(temporary, destination)
329
+ ensure
330
+ FileUtils.rm_f(temporary)
331
+ end
332
+
333
+ def verify_copy!(path, digest)
334
+ return if Digest::SHA256.file(path).hexdigest == digest
335
+
336
+ raise MarketplaceError, "widget package checksum mismatch: #{path}"
337
+ end
338
+
339
+ def write_lock(path, package, inventory, digest, destination, cache,
340
+ asset_original:, replaced_name: nil)
341
+ lock = OfficialMarketplace.lock(@target)
342
+ lock.fetch('packages').delete(replaced_name) if replaced_name && replaced_name != package.name
343
+ lock.fetch('packages')[package.name] = {
344
+ 'kind' => 'widget', 'version' => package.version, 'source' => package.source,
345
+ 'repository' => package.repository, 'sha256' => digest,
346
+ 'destination' => relative(destination), 'archive' => relative(cache),
347
+ 'files' => [relative(destination)], 'asset_original' => asset_original,
348
+ 'widget_name' => inventory.name, 'widget_ids' => inventory.widget_ids,
349
+ 'content_id' => package.content_id, 'version_id' => package.version_id,
350
+ 'content_type' => package.content_type, 'version_type' => package.version_type,
351
+ 'security_issues' => package.security_issues, 'private' => package.private,
352
+ 'company_approved' => package.company_approved
353
+ }.compact
354
+ temporary = "#{path}.tmp-#{Process.pid}"
355
+ FileUtils.mkdir_p(File.dirname(path))
356
+ File.write(temporary, JSON.pretty_generate(lock) << "\n")
357
+ File.rename(temporary, path)
358
+ ensure
359
+ FileUtils.rm_f(temporary)
360
+ end
361
+
362
+ def with_rollback(paths)
363
+ Dir.mktmpdir('mxrb-widget-rollback-') do |temporary|
364
+ snapshots = paths.each_with_index.map { |path, index| snapshot(path, temporary, index) }
365
+ yield
366
+ rescue StandardError
367
+ snapshots.reverse_each { restore_snapshot(_1) }
368
+ raise
369
+ end
370
+ end
371
+
372
+ def snapshot(path, temporary, index)
373
+ backup = File.join(temporary, index.to_s)
374
+ type = File.file?(path) ? :file : :missing
375
+ FileUtils.cp(path, backup) if type == :file
376
+ [path, backup, type]
377
+ end
378
+
379
+ def restore_snapshot(snapshot)
380
+ path, backup, type = snapshot
381
+ FileUtils.rm_f(path)
382
+ return if type == :missing
383
+
384
+ FileUtils.mkdir_p(File.dirname(path))
385
+ FileUtils.cp(backup, path)
386
+ end
387
+
388
+ def cache_relative(inventory, package)
389
+ version = package.version.to_s.gsub(/[^A-Za-z0-9_.-]/, '_')
390
+ stem = File.basename(inventory.project_filename, '.mpk')
391
+ File.join('.mxrb', 'marketplace', "#{stem}-#{version}.mpk")
392
+ end
393
+
394
+ def safe_path(relative)
395
+ path = File.expand_path(File.join(@target, relative))
396
+ prefix = "#{@target}#{File::SEPARATOR}"
397
+ raise MarketplaceError, "path is outside marketplace target: #{path}" unless path.start_with?(prefix)
398
+
399
+ path
400
+ end
401
+
402
+ def relative(path) = path.delete_prefix("#{@target}#{File::SEPARATOR}")
403
+
404
+ def ensure_no_symlink_components(path)
405
+ current = @target
406
+ relative(path).split(File::SEPARATOR)[0...-1].each do |part|
407
+ current = File.join(current, part)
408
+ raise MarketplaceError, "widget destination traverses a symbolic link: #{current}" if File.symlink?(current)
409
+ end
410
+ end
411
+ end
412
+ # rubocop:enable Metrics
413
+ end
414
+ end