rea-agents 6.2.0 → 6.3.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 (297) hide show
  1. package/README.md +18 -20
  2. package/bridge/hopper_bridge.py +2 -0
  3. package/dist/android/JadxProvider.js +8 -2
  4. package/dist/android/JadxSession.js +51 -9
  5. package/dist/application/BinaryTargetResolver.js +63 -17
  6. package/dist/application/BrowserScenarioEvidence.js +2 -2
  7. package/dist/application/ClientConfigurationDocument.js +28 -0
  8. package/dist/application/ClientRegistrationStatus.js +19 -6
  9. package/dist/application/DirectAnalysis.js +12 -6
  10. package/dist/application/DirectAnalysisCleanup.js +0 -1
  11. package/dist/application/DirectAnalysisStatus.js +2 -5
  12. package/dist/application/Doctor.js +25 -34
  13. package/dist/application/DoctorDiagnostics.js +1 -1
  14. package/dist/application/EvidenceBundleFiles.js +1 -1
  15. package/dist/application/InvestigationProviders.js +4 -4
  16. package/dist/application/JsonFiles.js +15 -15
  17. package/dist/application/LinuxHopper.js +2 -1
  18. package/dist/application/ReconstructionObligationCandidates.js +9 -2
  19. package/dist/application/ReconstructionObligationLedgerProjection.js +3 -1
  20. package/dist/application/RegularFileRead.js +17 -0
  21. package/dist/application/Setup.js +18 -7
  22. package/dist/application/SetupClientConfiguration.js +18 -7
  23. package/dist/application/SetupHost.js +6 -4
  24. package/dist/application/SetupPlan.js +19 -15
  25. package/dist/application/SetupSkill.js +57 -8
  26. package/dist/application/SupportedClients.js +43 -0
  27. package/dist/application/Uninstall.js +18 -2
  28. package/dist/application/UpdateMaintenance.js +6 -2
  29. package/dist/application/Utf8JsonInput.js +64 -0
  30. package/dist/application/WebScriptExportService.js +13 -6
  31. package/dist/application/android/AndroidAnalysisService.js +1 -1
  32. package/dist/application/binary/AnalysisSnapshotCache.js +5 -4
  33. package/dist/application/binary/BinarySession.js +16 -0
  34. package/dist/application/binary/BinarySessionOpen.js +9 -1
  35. package/dist/application/javascript/ElectronActiveEvidence.js +2 -2
  36. package/dist/application/javascript/ElectronBoundaryAnalysis.js +2 -2
  37. package/dist/application/javascript/ElectronBoundaryGraphNative.js +5 -1
  38. package/dist/application/javascript/JavaScriptApplicationEvidence.js +1 -0
  39. package/dist/application/javascript/JavaScriptApplicationEvidenceGraph.js +107 -46
  40. package/dist/application/javascript/JavaScriptApplicationService.js +1 -0
  41. package/dist/application/javascript/JavaScriptApplicationWorkflowService.js +59 -45
  42. package/dist/application/javascript/JavaScriptArtifactAnalysis.js +0 -6
  43. package/dist/application/javascript/JavaScriptArtifactGraphBuilder.js +8 -18
  44. package/dist/application/javascript/JavaScriptArtifactGraphFindings.js +34 -26
  45. package/dist/application/javascript/JavaScriptArtifactPathResolution.js +44 -12
  46. package/dist/application/javascript/JavaScriptArtifactReconstruction.js +6 -3
  47. package/dist/application/javascript/JavaScriptArtifactReconstructionInput.js +1 -0
  48. package/dist/application/javascript/JavaScriptJsonModules.js +2 -2
  49. package/dist/application/javascript/JavaScriptModuleRelationships.js +5 -17
  50. package/dist/application/javascript/JavaScriptRuntimeReconciliationService.js +11 -27
  51. package/dist/application/javascript/JavaScriptSemanticGraphAsyncProjection.js +2 -2
  52. package/dist/application/javascript/JavaScriptSemanticGraphBuilder.js +8 -6
  53. package/dist/application/javascript/JavaScriptSemanticGraphChildProcessProjection.js +1 -1
  54. package/dist/application/javascript/JavaScriptSemanticGraphConstruction.js +7 -5
  55. package/dist/application/javascript/JavaScriptSemanticGraphDataProjection.js +1 -1
  56. package/dist/application/javascript/JavaScriptSemanticGraphEvidence.js +1 -1
  57. package/dist/application/javascript/JavaScriptSemanticGraphFingerprintProjection.js +2 -2
  58. package/dist/application/javascript/JavaScriptSemanticGraphFlowProjection.js +2 -2
  59. package/dist/application/javascript/JavaScriptSemanticGraphObjectProjection.js +1 -1
  60. package/dist/application/javascript/JavaScriptSemanticGraphProjection.js +4 -4
  61. package/dist/application/javascript/JavaScriptSemanticGraphResourceProjection.js +1 -1
  62. package/dist/application/javascript/JavaScriptSemanticGraphValueProjection.js +10 -7
  63. package/dist/application/javascript/JavaScriptSemanticTraceService.js +15 -10
  64. package/dist/application/native/NativeCallRoutes.js +2 -4
  65. package/dist/artifacts/ArtifactProvider.js +14 -16
  66. package/dist/artifacts/ArtifactProviderMetadata.js +1 -4
  67. package/dist/artifacts/AsarArtifactReader.js +81 -17
  68. package/dist/artifacts/DirectoryArtifactReader.js +2 -2
  69. package/dist/artifacts/NativeDmgArtifactReader.js +152 -67
  70. package/dist/artifacts/NativeDmgOwnership.js +109 -0
  71. package/dist/artifacts/apple/KeyedArchiveReader.js +54 -46
  72. package/dist/artifacts/apple/MachoLoadCommandReader.js +36 -6
  73. package/dist/artifacts/extraction/ArtifactExtraction.js +29 -11
  74. package/dist/artifacts/inventory/ArtifactGraphConstruction.js +7 -8
  75. package/dist/artifacts/inventory/classify.js +33 -14
  76. package/dist/artifacts/inventory/hashStableRootArtifact.js +37 -0
  77. package/dist/artifacts/inventory/scanCanonical.js +28 -22
  78. package/dist/artifacts/inventory/scanReader.js +7 -11
  79. package/dist/artifacts/javascript/JavaScriptArtifactFiles.js +65 -27
  80. package/dist/browser/CdpBrowserProvider.js +4 -3
  81. package/dist/browser/CdpCaptureStorageFingerprints.js +3 -3
  82. package/dist/browser/CdpElectronInspection.js +2 -2
  83. package/dist/browser/CdpElectronProvider.js +4 -3
  84. package/dist/browser/CdpElectronScripts.js +2 -2
  85. package/dist/browser/CdpWebMcpDiscovery.js +16 -13
  86. package/dist/browser/PlaywrightBrowserScenarioProvider.js +6 -6
  87. package/dist/browser/PlaywrightScenarioBrowser.js +145 -42
  88. package/dist/browser/PlaywrightScenarioSession.js +3 -11
  89. package/dist/browser/WebSourceMapFetcher.js +32 -6
  90. package/dist/browser/assets/ScenarioScriptCapture.js +8 -12
  91. package/dist/cli/coreAnalysisCommands.js +2 -2
  92. package/dist/cli/coreBinaryCommands.js +13 -9
  93. package/dist/cli/electronCommands.js +9 -10
  94. package/dist/cli/javascriptApplicationAnalysis.js +2 -12
  95. package/dist/cli/jsonOutput.js +3 -2
  96. package/dist/cli/streamedJsonOutput.js +83 -4
  97. package/dist/cli/utilityCommands.js +2 -4
  98. package/dist/cli.js +5 -2
  99. package/dist/cliJsonFile.js +179 -0
  100. package/dist/cliJsonInput.js +47 -34
  101. package/dist/cliObservationOptions.js +4 -0
  102. package/dist/composition/auxiliaryAnalysisProviders.js +5 -4
  103. package/dist/composition/browserScenario.js +2 -2
  104. package/dist/composition/directAnalysis.js +2 -3
  105. package/dist/contracts/artifactComparisonExample.js +5 -5
  106. package/dist/contracts/errorSchemas.js +3 -1
  107. package/dist/contracts/functionComparisonExample.js +8 -0
  108. package/dist/contracts/inputSchemaPresentation.js +94 -3
  109. package/dist/contracts/javascript/electronToolContracts.js +2 -1
  110. package/dist/contracts/javascript/javascriptApplicationWorkflowExamples.js +3 -2
  111. package/dist/contracts/javascript/javascriptExportShapeComparisonExample.js +6 -3
  112. package/dist/contracts/javascript/javascriptRuntimeReconciliationExample.js +8 -7
  113. package/dist/contracts/managed/managedToolContracts.js +3 -2
  114. package/dist/contracts/mobileApplicationGraphExamples.js +5 -5
  115. package/dist/contracts/native/nativeToolContracts.js +1 -1
  116. package/dist/contracts/sessionLifecycleInputs.js +4 -3
  117. package/dist/contracts/sessionStatusContract.js +3 -2
  118. package/dist/contracts/sessionToolContracts.js +3 -2
  119. package/dist/contracts/sessionToolSchemas.js +3 -2
  120. package/dist/contracts/toolOutputSchemaGroups.js +2 -2
  121. package/dist/contracts/toolOutputSchemaPrimitives.js +2 -5
  122. package/dist/domain/analysisErrorBase.js +5 -0
  123. package/dist/domain/analysisErrorPresentation.js +4 -2
  124. package/dist/domain/analysisSnapshot.js +3 -1
  125. package/dist/domain/analysisView/analysisView.js +1 -3
  126. package/dist/domain/android/androidPartialObservation.js +36 -0
  127. package/dist/domain/apple/dylibResolution.js +1 -1
  128. package/dist/domain/artifactComparison.js +4 -4
  129. package/dist/domain/artifactGraph.js +1 -1
  130. package/dist/domain/artifactIdentity.js +8 -8
  131. package/dist/domain/artifactInspection.js +8 -8
  132. package/dist/domain/artifactInventoryEvidence.js +5 -5
  133. package/dist/domain/browserCompleteness.js +3 -2
  134. package/dist/domain/browserScenarioCaptureValues.js +2 -2
  135. package/dist/domain/browserScenarioNormalization.js +3 -3
  136. package/dist/domain/browserScenarioValues.js +3 -2
  137. package/dist/domain/callPath.js +8 -10
  138. package/dist/domain/canonicalDigest.js +7 -1
  139. package/dist/domain/canonicalOrdering.js +1 -13
  140. package/dist/domain/changedBehavior.js +2 -3
  141. package/dist/domain/comparisonSemantics.js +0 -5
  142. package/dist/domain/digests.js +0 -3
  143. package/dist/domain/evidence.js +10 -5
  144. package/dist/domain/evidenceBundle.js +51 -33
  145. package/dist/domain/firmware/firmwareAnalysis.js +4 -3
  146. package/dist/domain/functionComparison.js +2 -1
  147. package/dist/domain/functionComparisonNormalization.js +4 -3
  148. package/dist/domain/functionComparisonResults.js +2 -2
  149. package/dist/domain/functionDossierEvidence.js +2 -1
  150. package/dist/domain/hopperValues.js +11 -15
  151. package/dist/domain/javascript/electronActiveObservation.js +4 -3
  152. package/dist/domain/javascript/electronStaticAnalysisBrowser.js +2 -2
  153. package/dist/domain/javascript/electronStaticAnalysisIpc.js +9 -1
  154. package/dist/domain/javascript/electronStaticAnalysisNative.js +5 -3
  155. package/dist/domain/javascript/electronStaticAnalysisValues.js +8 -4
  156. package/dist/domain/javascript/javascriptApplicationAnalysis.js +3 -1
  157. package/dist/domain/javascript/javascriptApplicationChangeGraph.js +12 -8
  158. package/dist/domain/javascript/javascriptApplicationEvidenceSchemas.js +4 -0
  159. package/dist/domain/javascript/javascriptApplicationGraph.js +15 -13
  160. package/dist/domain/javascript/javascriptApplicationVersionComparison.js +4 -4
  161. package/dist/domain/javascript/javascriptApplicationVersionComparisonSchemas.js +37 -22
  162. package/dist/domain/javascript/javascriptApplicationVersionItems.js +45 -34
  163. package/dist/domain/javascript/javascriptApplicationVersionKeys.js +7 -7
  164. package/dist/domain/javascript/javascriptAstFingerprint.js +2 -2
  165. package/dist/domain/javascript/javascriptAstValues.js +22 -0
  166. package/dist/domain/javascript/javascriptExportShapeComparison.js +3 -2
  167. package/dist/domain/javascript/javascriptExportShapeComparisonSchemas.js +31 -23
  168. package/dist/domain/javascript/javascriptExportShapeSelection.js +2 -2
  169. package/dist/domain/javascript/javascriptExportShapeVariants.js +3 -2
  170. package/dist/domain/javascript/javascriptFeatureSeed.js +5 -3
  171. package/dist/domain/javascript/javascriptFeatureTrace.js +7 -6
  172. package/dist/domain/javascript/javascriptFeatureTraceSchemas.js +17 -9
  173. package/dist/domain/javascript/javascriptFeatureTraversal.js +4 -4
  174. package/dist/domain/javascript/javascriptNativeHandoff.js +6 -6
  175. package/dist/domain/javascript/javascriptRuntimeObservation.js +8 -5
  176. package/dist/domain/javascript/javascriptRuntimeReconciliation.js +12 -6
  177. package/dist/domain/javascript/javascriptRuntimeReconciliationActive.js +9 -8
  178. package/dist/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.js +212 -0
  179. package/dist/domain/javascript/javascriptRuntimeReconciliationGraph.js +5 -4
  180. package/dist/domain/javascript/javascriptRuntimeReconciliationInputValidation.js +28 -0
  181. package/dist/domain/javascript/javascriptRuntimeReconciliationMatching.js +5 -5
  182. package/dist/domain/javascript/javascriptRuntimeReconciliationParsing.js +12 -223
  183. package/dist/domain/javascript/javascriptRuntimeReconciliationResult.js +2 -2
  184. package/dist/domain/javascript/javascriptRuntimeReconciliationRuntime.js +15 -4
  185. package/dist/domain/javascript/javascriptRuntimeReconciliationSchemas.js +2 -2
  186. package/dist/domain/javascript/javascriptRuntimeStaticCandidates.js +7 -6
  187. package/dist/domain/javascript/javascriptSemanticAnalysis.js +200 -8
  188. package/dist/domain/javascript/javascriptSemanticCallResolution.js +9 -7
  189. package/dist/domain/javascript/javascriptSemanticChildProcesses.js +2 -2
  190. package/dist/domain/javascript/javascriptSemanticCoverage.js +20 -0
  191. package/dist/domain/javascript/javascriptSemanticDataEffectHelpers.js +7 -9
  192. package/dist/domain/javascript/javascriptSemanticDataEffects.js +4 -11
  193. package/dist/domain/javascript/javascriptSemanticFingerprints.js +2 -2
  194. package/dist/domain/javascript/javascriptSemanticGraph.js +142 -35
  195. package/dist/domain/javascript/javascriptSemanticGraphSchemas.js +19 -1
  196. package/dist/domain/javascript/javascriptSemanticIr.js +0 -4
  197. package/dist/domain/javascript/javascriptSemanticIterationSources.js +106 -0
  198. package/dist/domain/javascript/javascriptSemanticMemberMutations.js +497 -113
  199. package/dist/domain/javascript/javascriptSemanticMutationInitializers.js +190 -0
  200. package/dist/domain/javascript/javascriptSemanticMutationValues.js +41 -27
  201. package/dist/domain/javascript/javascriptSemanticObjects.js +2 -6
  202. package/dist/domain/javascript/javascriptSemanticPrimitives.js +7 -2
  203. package/dist/domain/javascript/javascriptSemanticProjection.js +20 -14
  204. package/dist/domain/javascript/javascriptSemanticPromises.js +15 -21
  205. package/dist/domain/javascript/javascriptSemanticPropertyPaths.js +23 -0
  206. package/dist/domain/javascript/javascriptSemanticProvenance.js +3 -3
  207. package/dist/domain/javascript/javascriptSemanticQuery.js +8 -2
  208. package/dist/domain/javascript/javascriptSemanticQueryIdentity.js +3 -2
  209. package/dist/domain/javascript/javascriptSemanticQuerySchemas.js +8 -9
  210. package/dist/domain/javascript/javascriptSemanticRequireOrigin.js +16 -10
  211. package/dist/domain/javascript/javascriptSemanticReturns.js +23 -27
  212. package/dist/domain/javascript/javascriptSemanticTraversal.js +6 -1
  213. package/dist/domain/javascript/javascriptSemanticValues.js +135 -24
  214. package/dist/domain/javascript/javascriptSourceMapPaths.js +11 -1
  215. package/dist/domain/javascript/javascriptSourceParser.js +34 -9
  216. package/dist/domain/javascript/javascriptStaticAnalysisBundler.js +5 -6
  217. package/dist/domain/javascript/javascriptStaticAnalysisHelpers.js +26 -15
  218. package/dist/domain/javascript/sourceToBundleComparison.js +7 -5
  219. package/dist/domain/javascript/sourceToBundleSignals.js +7 -7
  220. package/dist/domain/jsonSerialization.js +109 -55
  221. package/dist/domain/jsonShape.js +46 -19
  222. package/dist/domain/localPath.js +10 -4
  223. package/dist/domain/managed/managedApplicationGraph.js +10 -6
  224. package/dist/domain/managed/managedApplicationGraphNodes.js +82 -57
  225. package/dist/domain/managed/managedMemberComparisonMatch.js +6 -7
  226. package/dist/domain/native/nativeCallObservation.js +2 -4
  227. package/dist/domain/native/nativeValueFlow.js +5 -11
  228. package/dist/domain/native/objcSwiftMetadata.js +15 -17
  229. package/dist/domain/process/processCapture.js +11 -10
  230. package/dist/domain/process/processCaptureCoverage.js +8 -5
  231. package/dist/domain/process/processCaptureExample.js +43 -0
  232. package/dist/domain/process/processCaptureValidation.js +4 -3
  233. package/dist/domain/process/processComparison.js +0 -23
  234. package/dist/domain/process/processScenario.js +3 -3
  235. package/dist/domain/process/processTraceEvaluation.js +4 -4
  236. package/dist/domain/process/processTraceSpecification.js +1 -3
  237. package/dist/domain/providerCleanupError.js +6 -2
  238. package/dist/domain/reconstructionCoverage.js +2 -1
  239. package/dist/domain/reconstructionUnknowns.js +4 -3
  240. package/dist/domain/reconstructionVerification.js +3 -2
  241. package/dist/domain/referenceSourceGraph.js +7 -7
  242. package/dist/domain/referenceSourceImportParsing.js +34 -45
  243. package/dist/domain/residualUnknown.js +3 -2
  244. package/dist/domain/staticRuntimeCorrelation.js +2 -1
  245. package/dist/domain/swiftSymbolClassification.js +6 -1
  246. package/dist/domain/symbolAnalysis.js +17 -2
  247. package/dist/domain/unicodeCodePointOrder.js +6 -0
  248. package/dist/domain/webBundleAnalyzerInspection.js +9 -1
  249. package/dist/domain/webCaptureDiff.js +71 -29
  250. package/dist/domain/webCaptureDiffSchemas.js +2 -8
  251. package/dist/domain/webInventory.js +3 -2
  252. package/dist/domain/webMcpDiscovery.js +4 -3
  253. package/dist/domain/webScriptExport.js +1 -1
  254. package/dist/dotnet/ManagedMemberInstructionDecoder.js +1 -2
  255. package/dist/dotnet/ManagedStaticProvider.js +10 -7
  256. package/dist/dotnet/ManagedStaticProviderMetadata.js +1 -4
  257. package/dist/filesystem/RegularFile.js +41 -0
  258. package/dist/generatedPackageMetadata.js +1 -1
  259. package/dist/ghidra/GhidraClient.js +14 -4
  260. package/dist/ghidra/GhidraInstallation.js +111 -10
  261. package/dist/ghidra/GhidraLauncher.js +43 -19
  262. package/dist/hopper/HopperCleanup.js +1 -1
  263. package/dist/ida/IdaOperationRunner.js +2 -0
  264. package/dist/ida/IdaSessionClient.js +4 -2
  265. package/dist/inspector/JavaScriptRuntimeScope.js +9 -5
  266. package/dist/inspector/V8InspectorCaptureProjection.js +46 -13
  267. package/dist/inspector/V8InspectorProvider.js +60 -8
  268. package/dist/main/shutdown.js +76 -32
  269. package/dist/main/transport.js +1 -1
  270. package/dist/native/AppleObjcDispatchFacets.js +3 -3
  271. package/dist/native/AppleObjcProperties.js +17 -7
  272. package/dist/native/LldbCallTracer.js +241 -75
  273. package/dist/native/NativeMacOSProvider.js +10 -5
  274. package/dist/native/NativeMacOSProviderMetadata.js +1 -2
  275. package/dist/process/ProcessOwnershipObservation.js +9 -13
  276. package/dist/process/capture/FilesystemSnapshot.js +88 -18
  277. package/dist/process/capture/ProcessCaptureLifecycle.js +13 -27
  278. package/dist/process/capture/ProcessFilesystemEffects.js +19 -4
  279. package/dist/process/capture/ProcessHarness.js +8 -21
  280. package/dist/server/analysisAdmission.js +8 -0
  281. package/dist/server/createServer.js +22 -23
  282. package/dist/server/registerApplicationTools/compareExportShapes.js +2 -1
  283. package/dist/server/registerApplicationTools/helpers.js +3 -2
  284. package/dist/server/registerArtifactTools.js +2 -2
  285. package/dist/server/registerCloseLifecycleTool.js +6 -4
  286. package/dist/server/registerEnhancedTools.js +78 -73
  287. package/dist/server/registerEvidenceTools.js +54 -46
  288. package/dist/server/registerManagedTools.js +7 -4
  289. package/dist/server/registerOfficialTools.js +31 -25
  290. package/dist/server/registerSessionTools.js +9 -3
  291. package/native/windows/build/manifest.json +1 -1
  292. package/package.json +6 -3
  293. package/skills/reverse-engineer-anything/SKILL.md +5 -5
  294. package/skills/reverse-engineer-anything/references/native-and-artifacts.md +9 -0
  295. package/dist/artifacts/inventory/policy.js +0 -2
  296. package/dist/domain/managed/managedApplicationGraphNodeCoverage.js +0 -6
  297. package/dist/server/registerNativeTools.js +0 -6
@@ -0,0 +1,109 @@
1
+ import { relative, resolve, sep } from "node:path";
2
+ import { parse } from "plist";
3
+ import { z } from "zod";
4
+ const systemEntitySchema = z.object({
5
+ "dev-entry": z.string().startsWith("/dev/"),
6
+ "mount-point": z.string().optional(),
7
+ });
8
+ const attachOutputSchema = z.object({
9
+ "system-entities": z.array(systemEntitySchema),
10
+ });
11
+ const infoOutputSchema = z.object({
12
+ images: z.array(z.object({
13
+ "system-entities": z.array(systemEntitySchema),
14
+ })),
15
+ });
16
+ /** Parse hdiutil's current image inventory. */
17
+ export const parseNativeDmgInfo = (stdout) => infoOutputSchema.parse(parse(stdout));
18
+ /** Parse hdiutil's attach response while preserving device and mount facts. */
19
+ export const parseNativeDmgAttachOutput = (stdout) => attachOutputSchema.parse(parse(stdout));
20
+ /** Classify only fresh image groups tied to the exclusively owned mount root. */
21
+ export const resolveNativeDmgOwnership = (input) => {
22
+ const rootedImages = input.info.images.filter((image) => image["system-entities"].some(({ "mount-point": mountPoint }) => mountPoint !== undefined && isBelowRoot(input.mountRoot, mountPoint)));
23
+ if (rootedImages.length > 1)
24
+ return {
25
+ status: "unknown",
26
+ reason: "multiple hdiutil image groups report mounts beneath the owned root",
27
+ };
28
+ const rootedImage = rootedImages[0];
29
+ if (rootedImage !== undefined) {
30
+ const hasOutsideMount = rootedImage["system-entities"].some(({ "mount-point": mountPoint }) => mountPoint !== undefined && !isBelowRoot(input.mountRoot, mountPoint));
31
+ const groupDevices = rootedImage["system-entities"].map(({ "dev-entry": device }) => device);
32
+ const devices = topLevelDevices(groupDevices);
33
+ const overlapsBaseline = groupDevices.some((device) => input.baselineDevices.has(device));
34
+ if (hasOutsideMount || overlapsBaseline || devices.length === 0)
35
+ return {
36
+ status: "unknown",
37
+ reason: hasOutsideMount
38
+ ? "the image group contains a mount outside the owned root"
39
+ : overlapsBaseline
40
+ ? "the image group includes a device present before attachment"
41
+ : "the owned mount group contains no detachable device",
42
+ };
43
+ return { status: "owned", devices, source: "info" };
44
+ }
45
+ if (input.attachOutputReturned) {
46
+ const attachOwnership = ownershipFromAttachOutput(input.attachOutput, input.mountRoot, input.baselineDevices);
47
+ if (attachOwnership.status === "owned") {
48
+ return attachOwnership;
49
+ }
50
+ if (attachOwnership.status === "unknown")
51
+ return attachOwnership;
52
+ }
53
+ const newlyListedDevice = input.info.images.some((image) => image["system-entities"].some(({ "dev-entry": device }) => !input.baselineDevices.has(device)));
54
+ if (newlyListedDevice)
55
+ return {
56
+ status: "unknown",
57
+ reason: "hdiutil lists devices absent from the pre-attach baseline, but none is tied uniquely to the owned root; ownership is unknown",
58
+ };
59
+ return { status: "none" };
60
+ };
61
+ const ownershipFromAttachOutput = (output, mountRoot, baselineDevices) => {
62
+ if (output === undefined)
63
+ return {
64
+ status: "unknown",
65
+ reason: "hdiutil attach succeeded but its ownership facts were malformed",
66
+ };
67
+ const entities = output["system-entities"];
68
+ const hasRootMount = entities.some(({ "mount-point": mountPoint }) => mountPoint !== undefined && isBelowRoot(mountRoot, mountPoint));
69
+ const hasOutsideMount = entities.some(({ "mount-point": mountPoint }) => mountPoint !== undefined && !isBelowRoot(mountRoot, mountPoint));
70
+ const devices = entities.map(({ "dev-entry": device }) => device);
71
+ const uniqueDevices = topLevelDevices(devices);
72
+ const overlapsBaseline = devices.some((device) => baselineDevices.has(device));
73
+ if (hasRootMount) {
74
+ if (hasOutsideMount || overlapsBaseline || uniqueDevices.length === 0)
75
+ return {
76
+ status: "unknown",
77
+ reason: hasOutsideMount
78
+ ? "the attach result includes a mount outside the owned root"
79
+ : overlapsBaseline
80
+ ? "the attach result includes a device present before attachment"
81
+ : "the attach result's owned mount has no detachable device",
82
+ };
83
+ return { status: "owned", devices: uniqueDevices, source: "attach" };
84
+ }
85
+ if (devices.length === 0)
86
+ return {
87
+ status: "unknown",
88
+ reason: "hdiutil attach succeeded without reporting device ownership facts",
89
+ };
90
+ if (devices.some((device) => !baselineDevices.has(device)))
91
+ return {
92
+ status: "unknown",
93
+ reason: "the attach result reports new devices without a mount beneath the owned root",
94
+ };
95
+ return { status: "none" };
96
+ };
97
+ const topLevelDevices = (devices) => {
98
+ const unique = [...new Set(devices)];
99
+ return unique.filter((device) => !unique.some((parent) => /^\/dev\/disk\d+$/u.test(parent) &&
100
+ device.startsWith(`${parent}s`) &&
101
+ /^\d+$/u.test(device.slice(parent.length + 1))));
102
+ };
103
+ const isBelowRoot = (root, path) => {
104
+ const child = relative(root, resolve(path));
105
+ return (child !== "" &&
106
+ child !== "." &&
107
+ child !== ".." &&
108
+ !child.startsWith(`..${sep}`));
109
+ };
@@ -23,7 +23,8 @@ export const decodeKeyedArchiveBytes = (bytes, selection) => {
23
23
  const parsed = binary
24
24
  ? { value: parseBinary(bytes), omittedPrototypeKeys: 0 }
25
25
  : parseXmlPropertyList(xmlText ?? "");
26
- const graph = projectKeyedArchive(normalizePlist(parsed.value), selection);
26
+ const { value: plistValue, unknownRealCount } = projectPlistValue(parsed.value);
27
+ const graph = projectKeyedArchive(plistValue, selection);
27
28
  const numbers = createPlistNumberProjection(binary
28
29
  ? binaryPlistNumberLiterals(bytes)
29
30
  : xmlPlistNumberLiterals(xmlText ?? ""));
@@ -48,15 +49,14 @@ export const decodeKeyedArchiveBytes = (bytes, selection) => {
48
49
  ...(parsed.omittedPrototypeKeys === 0
49
50
  ? []
50
51
  : [omittedPrototypeKeysLimitation(parsed.omittedPrototypeKeys)]),
52
+ ...(unknownRealCount === 0
53
+ ? []
54
+ : [
55
+ `${String(unknownRealCount)} non-finite real value(s) are reported as { "$plist_type": "real", "value": null } because JSON evidence cannot preserve NaN vs infinity.`,
56
+ ]),
51
57
  ],
52
58
  };
53
59
  };
54
- const normalizePlist = (value) => {
55
- const projected = projectPlistValue(value);
56
- if (projected.unknownRealCount > 0)
57
- throw new RangeError("Keyed archive contains a non-finite real");
58
- return projected.value;
59
- };
60
60
  /**
61
61
  * Point to the workflow that reads the selected kind of file on this host.
62
62
  * decode_interface_builder is portable; inspect_plist requires macOS.
@@ -80,51 +80,59 @@ export const inspectBundleKeyedArchive = async (input) => {
80
80
  : `Archive path must be a canonical relative bundle path without empty, '.', or '..' segments: ${selected.path}`);
81
81
  const reader = new DirectoryArtifactReader(input.bundlePath);
82
82
  try {
83
+ // Match inventory spelling while retaining the raw path for I/O and evidence.
84
+ const wanted = selected.path.normalize("NFC");
85
+ const matches = [];
83
86
  for await (const entry of reader.entries(input.signal)) {
84
- if (entry.path !== selected.path)
87
+ if (entry.path.normalize("NFC") !== wanted)
85
88
  continue;
86
- if (entry.kind !== "file")
87
- throw archivePathError("invalid_value", `Archive path selects a ${entry.kind}, not a regular file: ${selected.path}`);
88
- if ((entry.declaredSize ?? 0) > MAX_BYTES)
89
+ matches.push(entry);
90
+ }
91
+ if (matches.length > 1)
92
+ throw archivePathError("invalid_value", `Archive path matches ${String(matches.length)} Unicode-equivalent bundle entries; selection is ambiguous: ${selected.path}`);
93
+ const entry = matches[0];
94
+ if (entry === undefined)
95
+ throw archivePathError("invalid_value", `No regular file exists at ${selected.path} in the active app bundle.`);
96
+ if (entry.kind !== "file")
97
+ throw archivePathError("invalid_value", `Archive path selects a ${entry.kind}, not a regular file: ${selected.path}`);
98
+ if ((entry.declaredSize ?? 0) > MAX_BYTES)
99
+ throw new ArtifactReaderFailure("limit", "Keyed archive exceeds 64 MiB");
100
+ const stream = await reader.open(entry, input.signal);
101
+ const chunks = [];
102
+ let size = 0;
103
+ for await (const chunk of stream) {
104
+ if (input.signal?.aborted) {
105
+ stream.destroy();
106
+ throw new ArtifactReaderFailure("cancelled", "Archive inspection cancelled");
107
+ }
108
+ const bytes = z.instanceof(Buffer).parse(chunk);
109
+ size += bytes.length;
110
+ if (size > MAX_BYTES) {
111
+ stream.destroy();
89
112
  throw new ArtifactReaderFailure("limit", "Keyed archive exceeds 64 MiB");
90
- const stream = await reader.open(entry, input.signal);
91
- const chunks = [];
92
- let size = 0;
93
- for await (const chunk of stream) {
94
- if (input.signal?.aborted) {
95
- stream.destroy();
96
- throw new ArtifactReaderFailure("cancelled", "Archive inspection cancelled");
97
- }
98
- const bytes = z.instanceof(Buffer).parse(chunk);
99
- size += bytes.length;
100
- if (size > MAX_BYTES) {
101
- stream.destroy();
102
- throw new ArtifactReaderFailure("limit", "Keyed archive exceeds 64 MiB");
103
- }
104
- chunks.push(bytes);
105
113
  }
106
- const bytes = Buffer.concat(chunks, size);
107
- try {
108
- return keyedArchiveResultSchema.parse({
109
- target_sha256: input.targetSha256,
110
- archive_path: entry.path,
111
- archive_sha256: createHash("sha256").update(bytes).digest("hex"),
112
- ...decodeKeyedArchiveBytes(bytes, selected),
114
+ chunks.push(bytes);
115
+ }
116
+ const bytes = Buffer.concat(chunks, size);
117
+ try {
118
+ return keyedArchiveResultSchema.parse({
119
+ target_sha256: input.targetSha256,
120
+ archive_path: entry.path,
121
+ archive_sha256: createHash("sha256").update(bytes).digest("hex"),
122
+ ...decodeKeyedArchiveBytes(bytes, selected),
123
+ });
124
+ }
125
+ catch (cause) {
126
+ if (cause instanceof AnalysisInputError)
127
+ throw cause;
128
+ // An intact file of another kind is not a damaged archive.
129
+ if (cause instanceof KeyedArchiveKindError)
130
+ throw new AnalysisUnsupportedTargetError("inspect_keyed_archive", join(input.bundlePath, entry.path), cause.message, {
131
+ cause,
132
+ remediationAction: keyedArchiveKindRemediation(cause.kind, input.platform ?? process.platform),
113
133
  });
114
- }
115
- catch (cause) {
116
- if (cause instanceof AnalysisInputError)
117
- throw cause;
118
- // An intact file of another kind is not a damaged archive.
119
- if (cause instanceof KeyedArchiveKindError)
120
- throw new AnalysisUnsupportedTargetError("inspect_keyed_archive", join(input.bundlePath, entry.path), cause.message, {
121
- cause,
122
- remediationAction: keyedArchiveKindRemediation(cause.kind, input.platform ?? process.platform),
123
- });
124
- throw new ArtifactReaderFailure(cause instanceof RangeError ? "limit" : "format", `Cannot decode selected Foundation keyed archive: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
125
- }
134
+ throw new ArtifactReaderFailure(cause instanceof RangeError ? "limit" : "format", `Cannot decode selected Foundation keyed archive: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
126
135
  }
127
- throw archivePathError("invalid_value", `No regular file exists at ${selected.path} in the active app bundle.`);
128
136
  }
129
137
  finally {
130
138
  await reader.close();
@@ -29,6 +29,41 @@ const LC_VERSION_MIN_IPHONEOS = 0x25;
29
29
  const LC_VERSION_MIN_TVOS = 0x2f;
30
30
  const LC_VERSION_MIN_WATCHOS = 0x30;
31
31
  const LC_BUILD_VERSION = 0x32;
32
+ // `mach-o/loader.h` `PLATFORM_*` numbers observed via Apple linker output.
33
+ const PLATFORM_MACOS = 1;
34
+ const PLATFORM_IOS = 2;
35
+ const PLATFORM_TVOS = 3;
36
+ const PLATFORM_WATCHOS = 4;
37
+ const PLATFORM_IOSSIMULATOR = 7;
38
+ const PLATFORM_TVOSSIMULATOR = 8;
39
+ const PLATFORM_WATCHOSSIMULATOR = 9;
40
+ const CPU_TYPE_I386 = 7;
41
+ const CPU_TYPE_X86_64 = 0x01000007;
42
+ /**
43
+ * Legacy `LC_VERSION_MIN_*` commands predate explicit simulator platform
44
+ * IDs. Per Apple's dyld rules the image CPU type distinguishes simulator
45
+ * from device: Intel images are simulator, ARM images are device
46
+ * (tvOS simulator applies to x86_64 only). `LC_BUILD_VERSION` platform
47
+ * numbers are preserved verbatim; unknown future numbers stay unknown
48
+ * downstream, never mapped to nearest.
49
+ */
50
+ const legacyVersionMinPlatform = (command, cpuType) => {
51
+ const isIntel = cpuType === CPU_TYPE_I386 || cpuType === CPU_TYPE_X86_64;
52
+ switch (command) {
53
+ case LC_VERSION_MIN_MACOSX:
54
+ return PLATFORM_MACOS;
55
+ case LC_VERSION_MIN_IPHONEOS:
56
+ return isIntel ? PLATFORM_IOSSIMULATOR : PLATFORM_IOS;
57
+ case LC_VERSION_MIN_TVOS:
58
+ return cpuType === CPU_TYPE_X86_64
59
+ ? PLATFORM_TVOSSIMULATOR
60
+ : PLATFORM_TVOS;
61
+ case LC_VERSION_MIN_WATCHOS:
62
+ return isIntel ? PLATFORM_WATCHOSSIMULATOR : PLATFORM_WATCHOS;
63
+ default:
64
+ return undefined;
65
+ }
66
+ };
32
67
  /** `mach-o/loader.h` `dylib_use_command`: `nameoff == 28` and this marker. */
33
68
  const DYLIB_USE_MARKER = 0x1a741800;
34
69
  const DYLIB_USE_WEAK_LINK = 0x1;
@@ -258,12 +293,7 @@ const decodeCommand = (command, body, index, slice) => {
258
293
  slice.platforms.push(platform);
259
294
  return;
260
295
  }
261
- const legacyPlatform = new Map([
262
- [LC_VERSION_MIN_MACOSX, 1],
263
- [LC_VERSION_MIN_IPHONEOS, 2],
264
- [LC_VERSION_MIN_TVOS, 3],
265
- [LC_VERSION_MIN_WATCHOS, 4],
266
- ]).get(command);
296
+ const legacyPlatform = legacyVersionMinPlatform(command, slice.cpu_type);
267
297
  if (legacyPlatform !== undefined) {
268
298
  if (body.byteLength < 16)
269
299
  throw new MachoFormatIssue("malformed", `load command ${index} is too short for a version-min command`);
@@ -1,6 +1,6 @@
1
1
  import { lstat, realpath } from "node:fs/promises";
2
- import { compareCodePoints } from "../../domain/canonicalOrdering.js";
3
- import { canonicalDigest } from "../../domain/comparisonSemantics.js";
2
+ import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
3
+ import { digestCanonicalValue } from "../../domain/canonicalDigest.js";
4
4
  import { AsarArtifactReader } from "../AsarArtifactReader.js";
5
5
  import { ArtifactPathRegistry, normalizeArtifactPath, } from "../ArtifactPaths.js";
6
6
  import { ArtifactReaderFailure, } from "../ArtifactReader.js";
@@ -22,9 +22,26 @@ export const extractArtifact = async (input, signal) => {
22
22
  signal,
23
23
  environment: input.environment,
24
24
  });
25
+ return materializeArtifactInventory(input, sourcePath, snapshot, signal);
26
+ };
27
+ /** Materialize a scanned inventory, verifying every current occurrence against it. */
28
+ export const materializeArtifactInventory = async (input, sourcePath, snapshot, signal) => {
29
+ abortIfNeeded(signal);
25
30
  const selectedOccurrences = snapshot.occurrences.filter((occurrence) => (occurrence.entry_kind === "file" || occurrence.entry_kind === "slice") &&
26
31
  occurrence.logical_path !== ".");
27
- const selectedIds = new Set(selectedOccurrences.map(({ occurrence_id: id }) => id));
32
+ const regularPaths = new Set(selectedOccurrences.map(({ logical_path: path }) => path));
33
+ const activeOccurrences = selectedOccurrences.filter(({ logical_path }) => {
34
+ // Nested members remain represented by their containing regular file;
35
+ // only entries exposed by the active reader are materialized.
36
+ let path = logical_path;
37
+ for (let slash = path.lastIndexOf("/"); slash >= 0; slash = path.lastIndexOf("/")) {
38
+ path = path.slice(0, slash);
39
+ if (regularPaths.has(path))
40
+ return false;
41
+ }
42
+ return true;
43
+ });
44
+ const selectedIds = new Set(activeOccurrences.map(({ occurrence_id: id }) => id));
28
45
  const occurrences = new Map();
29
46
  const neededNodes = new Set();
30
47
  collectOccurrences(snapshot.occurrences, selectedIds, occurrences, neededNodes);
@@ -35,7 +52,7 @@ export const extractArtifact = async (input, signal) => {
35
52
  occurrences,
36
53
  nodes,
37
54
  };
38
- const selected = selectedOccurrences.map((occurrence) => {
55
+ const selected = activeOccurrences.map((occurrence) => {
39
56
  // REA never decrypts archive entries, so an encrypted entry makes the
40
57
  // complete extraction unsupported rather than the archive invalid.
41
58
  if (occurrence.encrypted)
@@ -66,7 +83,6 @@ const materializeSelection = async ({ input, sourcePath, inventory, selected, si
66
83
  const extracted = [];
67
84
  try {
68
85
  output = await SafeOutputTree.create(input.outputRoot);
69
- const materialized = [];
70
86
  const registry = new ArtifactPathRegistry();
71
87
  for await (const entry of reader.entries(signal)) {
72
88
  const path = normalizeArtifactPath(entry.path);
@@ -87,13 +103,15 @@ const materializeSelection = async ({ input, sourcePath, inventory, selected, si
87
103
  bytes_written: written.bytesWritten,
88
104
  created: true,
89
105
  });
90
- materialized.push(selectedItem);
106
+ byPath.delete(path);
91
107
  }
108
+ if (byPath.size > 0)
109
+ throw new ArtifactReaderFailure("integrity", `Inventoried regular artifact entries were not materialized: ${[...byPath.keys()].sort(compareUnicodeCodePoints).join(", ")}`);
92
110
  readerCloseAttempted = true;
93
111
  await reader.close();
94
112
  readerClosed = true;
95
- extracted.sort((left, right) => compareCodePoints(left.relative_path, right.relative_path));
96
- const result = createExtractionResult(input, inventory, materialized, extracted);
113
+ extracted.sort((left, right) => compareUnicodeCodePoints(left.relative_path, right.relative_path));
114
+ const result = createExtractionResult(input, inventory, selected, extracted);
97
115
  await output.commit();
98
116
  return result;
99
117
  }
@@ -144,15 +162,15 @@ const createExtractionResult = (input, inventory, selected, extracted) => {
144
162
  source_manifest_id: inventory.manifest.manifest_id,
145
163
  selected_occurrence_ids: selected
146
164
  .map(({ occurrence }) => occurrence.occurrence_id)
147
- .sort(compareCodePoints),
148
- files_sha256: canonicalDigest(extracted, "Artifact"),
165
+ .sort(compareUnicodeCodePoints),
166
+ files_sha256: digestCanonicalValue(extracted, "Artifact"),
149
167
  output_root_alias: "$OUTPUT_ROOT",
150
168
  };
151
169
  return artifactExtractionResultSchema.parse({
152
170
  manifest: inventory.manifest,
153
171
  extraction_manifest: {
154
172
  ...extractionSemantic,
155
- extraction_id: `aex_${canonicalDigest(extractionSemantic, "Artifact")}`,
173
+ extraction_id: `aex_${digestCanonicalValue(extractionSemantic, "Artifact")}`,
156
174
  },
157
175
  output_root: input.outputRoot,
158
176
  artifacts: extracted,
@@ -1,5 +1,5 @@
1
- import { compareCodePoints } from "../../domain/canonicalOrdering.js";
2
- import { canonicalDigest } from "../../domain/comparisonSemantics.js";
1
+ import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
2
+ import { digestCanonicalValue } from "../../domain/canonicalDigest.js";
3
3
  import { artifactEdgeId, artifactIdForContent, occurrenceIdForLocation, } from "../../domain/artifactIdentity.js";
4
4
  import { hasZipSignature, zipPackageFormatForPath, } from "../../domain/zipPackageFormat.js";
5
5
  import { mzWindowsHeaderOffset, parseDosMzHeader } from "../../domain/dosMz.js";
@@ -12,7 +12,7 @@ export const indexOccurrencesByPath = (occurrences) => {
12
12
  return byPath;
13
13
  };
14
14
  export const createOccurrence = (entry, path, parent) => ({
15
- occurrence_id: `occ_${canonicalDigest({ path, kind: entry.kind }, "Artifact")}`,
15
+ occurrence_id: `occ_${digestCanonicalValue({ path, kind: entry.kind }, "Artifact")}`,
16
16
  artifact_id: null,
17
17
  parent_occurrence_id: parent,
18
18
  logical_path: path,
@@ -36,7 +36,6 @@ export const createOccurrence = (entry, path, parent) => ({
36
36
  limitations: [...entry.limitations],
37
37
  });
38
38
  /** Compare exact child names independently of host locale and traversal order. */
39
- export const compareDirectoryChildNames = compareCodePoints;
40
39
  export const materializeDirectoryNodes = (occurrences, nodes) => {
41
40
  const directories = occurrences
42
41
  .filter(({ entry_kind: kind }) => kind === "directory")
@@ -59,9 +58,9 @@ export const materializeDirectoryNodes = (occurrences, nodes) => {
59
58
  artifact_id,
60
59
  entry_kind,
61
60
  }))
62
- .sort((left, right) => compareDirectoryChildNames(left.name, right.name));
61
+ .sort((left, right) => compareUnicodeCodePoints(left.name, right.name));
63
62
  const node = createArtifactNode({
64
- sha256: canonicalDigest({ kind: "directory", children }, "Artifact"),
63
+ sha256: digestCanonicalValue({ kind: "directory", children }, "Artifact"),
65
64
  size: 0,
66
65
  format: "directory",
67
66
  contentState: "virtual",
@@ -73,14 +72,14 @@ export const materializeDirectoryNodes = (occurrences, nodes) => {
73
72
  };
74
73
  export const createRootNode = (input) => createArtifactNode({
75
74
  sha256: input.digest?.sha256 ??
76
- canonicalDigest({
75
+ digestCanonicalValue({
77
76
  kind: "directory-root",
78
77
  children: input.occurrences
79
78
  .map(({ logical_path, artifact_id }) => ({
80
79
  logical_path,
81
80
  artifact_id,
82
81
  }))
83
- .sort((left, right) => compareDirectoryChildNames(left.logical_path, right.logical_path)),
82
+ .sort((left, right) => compareUnicodeCodePoints(left.logical_path, right.logical_path)),
84
83
  }, "Artifact"),
85
84
  size: input.digest?.bytes ?? 0,
86
85
  format: input.digest === null
@@ -1,7 +1,9 @@
1
- import { open } from "node:fs/promises";
1
+ import { NonRegularFileReadError, openRegularFile, sameRegularFileState, } from "../../filesystem/RegularFile.js";
2
2
  import { classifyArtifactContent } from "./ArtifactGraphConstruction.js";
3
3
  import { ARTIFACT_CLASSIFICATION_PREFIX_BYTES } from "../ArtifactHash.js";
4
4
  import { hasZipSignature, zipPackageFormatForPath, } from "../../domain/zipPackageFormat.js";
5
+ import { ArtifactReaderFailure } from "../ArtifactReader.js";
6
+ import { hashStableRootArtifactHandle } from "./hashStableRootArtifact.js";
5
7
  const classifyContainerExtension = (path) => {
6
8
  const lower = path.toLowerCase();
7
9
  const zipPackage = zipPackageFormatForPath(lower);
@@ -12,23 +14,40 @@ const classifyContainerExtension = (path) => {
12
14
  return format;
13
15
  return undefined;
14
16
  };
15
- export const classifyRoot = async (path, directory) => {
17
+ /** Classify and hash one file root through the same stable open descriptor. */
18
+ export const classifyAndHashRoot = async (path, directory, expectedMetadata, signal) => {
16
19
  if (directory)
17
- return "directory";
18
- const extensionFormat = classifyContainerExtension(path);
19
- if (extensionFormat !== undefined)
20
- return extensionFormat;
21
- const handle = await open(path, "r");
20
+ return { format: "directory", digest: null };
21
+ const handle = await openRootFile(path, signal);
22
22
  try {
23
- const magic = Buffer.alloc(ARTIFACT_CLASSIFICATION_PREFIX_BYTES);
24
- const observed = await handle.read(magic, 0, magic.length, 0);
25
- const prefix = magic.subarray(0, observed.bytesRead);
26
- if (hasZipSignature(prefix))
27
- return "zip";
28
- return classifyArtifactContent(path, prefix, (await handle.stat()).size)
29
- .format;
23
+ const initial = await handle.stat();
24
+ if (!sameRegularFileState(expectedMetadata, initial))
25
+ throw new ArtifactReaderFailure("integrity", `Root artifact changed before inventory: ${path}`);
26
+ const format = classifyContainerExtension(path) ??
27
+ (await classifyOpenedRoot(path, handle));
28
+ const digest = await hashStableRootArtifactHandle(path, handle, initial, signal);
29
+ return { format, digest };
30
30
  }
31
31
  finally {
32
32
  await handle.close();
33
33
  }
34
34
  };
35
+ const openRootFile = async (path, signal) => {
36
+ try {
37
+ return await openRegularFile(path, { symlinks: "reject", signal });
38
+ }
39
+ catch (cause) {
40
+ if (cause instanceof NonRegularFileReadError)
41
+ throw new ArtifactReaderFailure("format", `Artifact root is not a regular file: ${path}`, { cause });
42
+ throw cause;
43
+ }
44
+ };
45
+ const classifyOpenedRoot = async (path, handle) => {
46
+ const magic = Buffer.alloc(ARTIFACT_CLASSIFICATION_PREFIX_BYTES);
47
+ const observed = await handle.read(magic, 0, magic.length, 0);
48
+ const prefix = magic.subarray(0, observed.bytesRead);
49
+ if (hasZipSignature(prefix))
50
+ return "zip";
51
+ return classifyArtifactContent(path, prefix, (await handle.stat()).size)
52
+ .format;
53
+ };
@@ -0,0 +1,37 @@
1
+ import { lstat } from "node:fs/promises";
2
+ import { ArtifactReaderFailure } from "../ArtifactReader.js";
3
+ import { NonRegularFileReadError, openRegularFile, sameRegularFileState, } from "../../filesystem/RegularFile.js";
4
+ import { abortIfNeeded, hashReadable, } from "../ArtifactHash.js";
5
+ /** Hash one stable regular root file from the same descriptor that was checked. */
6
+ export const hashStableRootArtifact = async (path, signal) => {
7
+ let handle;
8
+ try {
9
+ handle = await openRegularFile(path, {
10
+ symlinks: "reject",
11
+ signal,
12
+ });
13
+ }
14
+ catch (cause) {
15
+ if (cause instanceof NonRegularFileReadError)
16
+ throw new ArtifactReaderFailure("format", `Artifact root is not a regular file: ${path}`, { cause });
17
+ throw cause;
18
+ }
19
+ try {
20
+ const initial = await handle.stat();
21
+ return await hashStableRootArtifactHandle(path, handle, initial, signal);
22
+ }
23
+ finally {
24
+ await handle.close();
25
+ }
26
+ };
27
+ /** Hash and revalidate the same open descriptor used for root classification. */
28
+ export const hashStableRootArtifactHandle = async (path, handle, initial, signal) => {
29
+ const digest = await hashReadable(handle.createReadStream({ autoClose: false }), signal);
30
+ abortIfNeeded(signal);
31
+ const [opened, currentPath] = await Promise.all([handle.stat(), lstat(path)]);
32
+ if (!sameRegularFileState(initial, opened) ||
33
+ !sameRegularFileState(initial, currentPath) ||
34
+ digest.bytes !== initial.size)
35
+ throw new ArtifactReaderFailure("integrity", `Root artifact changed during inventory: ${path}`);
36
+ return digest;
37
+ };
@@ -1,26 +1,27 @@
1
- import { compareCodePoints } from "../../domain/canonicalOrdering.js";
2
- import { createReadStream } from "node:fs";
1
+ import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
2
+ import { AsarArtifactReader } from "../AsarArtifactReader.js";
3
3
  import { lstat } from "node:fs/promises";
4
4
  import { ArtifactReaderFailure, } from "../ArtifactReader.js";
5
5
  import { artifactInventoryResultSchema, } from "../../domain/artifactGraph.js";
6
6
  import { createArtifactEdges, createRootNode, indexOccurrencesByPath, materializeDirectoryNodes, rekeyOccurrences, rootOccurrenceFor, } from "./ArtifactGraphConstruction.js";
7
7
  import { artifactContradictionId, artifactGraphDigest, artifactManifestId, } from "../../domain/artifactIdentity.js";
8
- import { classifyRoot } from "./classify.js";
9
- import { hashReadable } from "../ArtifactHash.js";
8
+ import { classifyAndHashRoot } from "./classify.js";
9
+ import {} from "../ArtifactHash.js";
10
+ import { hashStableRootArtifact } from "./hashStableRootArtifact.js";
10
11
  import { createReader, inventoryLimitations } from "./reader.js";
11
12
  import { scanReader, } from "./scanReader.js";
12
13
  import { STRICT_INTEGRITY_POLICY, } from "./types.js";
13
14
  export const scanCanonicalArtifactInventory = async (path, options = {}, readerFactory = createReader) => {
14
15
  const integrity = options.integrity ?? STRICT_INTEGRITY_POLICY;
15
16
  const metadata = await lstat(path);
16
- const rootFormat = await classifyRoot(path, metadata.isDirectory());
17
- const rootDigest = metadata.isDirectory()
18
- ? null
19
- : await hashReadable(createReadStream(path), options.signal);
17
+ const { format: rootFormat, digest: rootDigest } = await classifyAndHashRoot(path, metadata.isDirectory(), metadata, options.signal);
20
18
  const reader = await readerFactory(path, rootFormat, options.environment ?? {}, options.signal);
19
+ const ownedReaders = reader === undefined ? [] : [reader];
21
20
  let outcome;
22
21
  try {
23
- const { nodes, occurrences, pendingContradictions } = await scanReader(reader, options.signal, integrity);
22
+ if (reader instanceof AsarArtifactReader)
23
+ await reader.prepareContainer(rootDigest?.sha256, options.signal);
24
+ const { nodes, occurrences, pendingContradictions } = await scanReader(reader, ownedReaders, options.signal, integrity);
24
25
  outcome = {
25
26
  kind: "completed",
26
27
  snapshot: await buildInventorySnapshot({
@@ -39,16 +40,21 @@ export const scanCanonicalArtifactInventory = async (path, options = {}, readerF
39
40
  catch (cause) {
40
41
  outcome = { kind: "failed", cause };
41
42
  }
42
- try {
43
- await reader?.close();
44
- }
45
- catch (cleanupCause) {
46
- const cleanup = ArtifactReaderFailure.cleanupObservation(cleanupCause, `artifact reader for ${path}`);
47
- const primary = outcome.kind === "failed" ? outcome.cause : cleanupCause;
48
- throw ArtifactReaderFailure.withCleanup(primary, cleanup, outcome.kind === "completed"
49
- ? { kind: "artifact-inventory", inventory: outcome.snapshot }
50
- : undefined);
43
+ let cleanupFailure;
44
+ for (const owned of ownedReaders.reverse()) {
45
+ try {
46
+ await owned.close();
47
+ }
48
+ catch (cleanupCause) {
49
+ const cleanup = ArtifactReaderFailure.cleanupObservation(cleanupCause, `artifact reader for ${path}`);
50
+ cleanupFailure = ArtifactReaderFailure.withCleanup(cleanupFailure ??
51
+ (outcome.kind === "failed" ? outcome.cause : cleanupCause), cleanup, outcome.kind === "completed"
52
+ ? { kind: "artifact-inventory", inventory: outcome.snapshot }
53
+ : undefined);
54
+ }
51
55
  }
56
+ if (cleanupFailure !== undefined)
57
+ throw cleanupFailure;
52
58
  if (outcome.kind === "failed")
53
59
  throw outcome.cause;
54
60
  return outcome.snapshot;
@@ -142,14 +148,14 @@ const buildIntegrityContradictions = (pending, occurrenceByPath, occurrenceById,
142
148
  const verifyRootDigest = async (path, rootDigest, signal) => {
143
149
  if (rootDigest === null)
144
150
  return;
145
- const verified = await hashReadable(createReadStream(path), signal);
151
+ const verified = await hashStableRootArtifact(path, signal);
146
152
  if (verified.sha256 !== rootDigest.sha256 ||
147
153
  verified.bytes !== rootDigest.bytes)
148
154
  throw new ArtifactReaderFailure("integrity", "Root artifact changed during inventory");
149
155
  };
150
- const sortNodes = (nodes) => nodes.sort((left, right) => compareCodePoints(left.artifact_id, right.artifact_id));
151
- const sortOccurrences = (occurrences) => occurrences.sort((left, right) => compareCodePoints(left.logical_path, right.logical_path));
152
- const sortEdges = (edges) => edges.sort((left, right) => compareCodePoints(left.edge_id, right.edge_id));
156
+ const sortNodes = (nodes) => nodes.sort((left, right) => compareUnicodeCodePoints(left.artifact_id, right.artifact_id));
157
+ const sortOccurrences = (occurrences) => occurrences.sort((left, right) => compareUnicodeCodePoints(left.logical_path, right.logical_path));
158
+ const sortEdges = (edges) => edges.sort((left, right) => compareUnicodeCodePoints(left.edge_id, right.edge_id));
153
159
  const buildManifest = ({ rootNode, rootFormat, graphSha256, orderedNodes, orderedOccurrences, orderedEdges, }) => artifactInventoryResultSchema.shape.manifest.parse({
154
160
  manifest_id: artifactManifestId(rootNode.artifact_id, graphSha256),
155
161
  root_artifact_id: rootNode.artifact_id,