diff --git a/.github/workflows/native-abi.yml b/.github/workflows/native-abi.yml index 949ab313..f5442d2c 100644 --- a/.github/workflows/native-abi.yml +++ b/.github/workflows/native-abi.yml @@ -153,7 +153,7 @@ jobs: cmake --install artifacts/cmake/win-x64/%%p || exit /b 1 ) - ctest --test-dir artifacts/cmake/win-x64/shim-static --output-on-failure -R "^(arc_native_abi_c17_layout_tests|arc_native_abi_cpp20_tests|arcpdf_abi_engine_tests|arcpdf_abi_unsupported_tests)$" || exit /b 1 + ctest --test-dir artifacts/cmake/win-x64/shim-static --output-on-failure -R "^(arc_native_abi_c17_layout_tests|arc_native_abi_cpp20_tests)$" || exit /b 1 dotnet test --project tests/NativeAbiTests/ArcForges.Tests.NativeAbiTests.csproj --filter Category=NativeAbiLayout --no-restore || exit /b 1 sccache --show-stats || exit /b 1 diff --git a/DesktopPlatform.slnx b/DesktopPlatform.slnx index c027242b..060fe348 100644 --- a/DesktopPlatform.slnx +++ b/DesktopPlatform.slnx @@ -70,6 +70,5 @@ - diff --git a/docs/native-reconciliation.md b/docs/native-reconciliation.md index 6789fb20..cbcf9196 100644 --- a/docs/native-reconciliation.md +++ b/docs/native-reconciliation.md @@ -13,4 +13,4 @@ P2-019/P2-020 and GOV.17 retire the native families consumed only by products ou The retained image dependency closure still includes OpenColorIO through OpenImageIO. Removing the owned Colour wrapper does not permit stripping dependencies or notices required by Image. New profile/record revisions bind only retained artifacts; original provenance and published versions remain unchanged. -Packaging guards reject tampered or missing Image/CRT DLLs, wrong RID, altered headers, incorrect package pairs and missing upstream notices. No native functionality beyond existing metadata/error probes is introduced. Instruments, functional image/PDF previews and sandbox composition remain their named delivery tasks. +Packaging guards reject tampered or missing Image/CRT DLLs, wrong RID, altered headers, incorrect package pairs and missing upstream notices. No native functionality beyond existing metadata/error probes is introduced. Instruments, functional image previews and sandbox composition remain their named delivery tasks; the PDF engine and local PDF parsing are retired by NAT.32 under P2-022 (recorded as retired, not completed). diff --git a/eng/policy/architecture-contract-tests.json b/eng/policy/architecture-contract-tests.json index 110124e7..82904a60 100644 --- a/eng/policy/architecture-contract-tests.json +++ b/eng/policy/architecture-contract-tests.json @@ -2824,31 +2824,6 @@ "testProject": "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ArcForges.ContentSandbox.Tests.csproj", "testMethod": "ArcForges.ContentSandbox.Tests.InvocationTests.ATileTravelsThroughAGrantedSlotAndIsVerifiedOnThePrivateCopy()" }, - { - "apiSymbol": "ArcForges.ContentSandbox.Broker.ContentSandboxInvocation.OpenPdfAsync(System.Threading.CancellationToken)", - "testProject": "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ArcForges.ContentSandbox.Tests.csproj", - "testMethod": "ArcForges.ContentSandbox.Tests.InvocationTests.APdfIsOpenedPagedAndExtractedInBoundedChunksAndRendered()" - }, - { - "apiSymbol": "ArcForges.ContentSandbox.Broker.ContentSandboxInvocation.GetPdfPageAsync(System.Guid, uint, System.Threading.CancellationToken)", - "testProject": "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ArcForges.ContentSandbox.Tests.csproj", - "testMethod": "ArcForges.ContentSandbox.Tests.InvocationTests.APdfIsOpenedPagedAndExtractedInBoundedChunksAndRendered()" - }, - { - "apiSymbol": "ArcForges.ContentSandbox.Broker.ContentSandboxInvocation.ExtractPdfTextAsync(System.Guid, uint, uint, System.Threading.CancellationToken)", - "testProject": "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ArcForges.ContentSandbox.Tests.csproj", - "testMethod": "ArcForges.ContentSandbox.Tests.InvocationTests.APdfIsOpenedPagedAndExtractedInBoundedChunksAndRendered()" - }, - { - "apiSymbol": "ArcForges.ContentSandbox.Broker.ContentSandboxInvocation.RenderPdfTileAsync(System.Guid, ArcForges.Contracts.LocalRpc.Sandbox.V1.SandboxPdfPage, uint, uint, uint, uint, uint, uint, System.Threading.CancellationToken)", - "testProject": "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ArcForges.ContentSandbox.Tests.csproj", - "testMethod": "ArcForges.ContentSandbox.Tests.InvocationTests.APdfIsOpenedPagedAndExtractedInBoundedChunksAndRendered()" - }, - { - "apiSymbol": "ArcForges.ContentSandbox.Broker.ContentSandboxInvocation.ClosePdfAsync(System.Guid, System.Threading.CancellationToken)", - "testProject": "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ArcForges.ContentSandbox.Tests.csproj", - "testMethod": "ArcForges.ContentSandbox.Tests.InvocationTests.APdfIsOpenedPagedAndExtractedInBoundedChunksAndRendered()" - }, { "apiSymbol": "ArcForges.ContentSandbox.Broker.ContentSandboxInvocation.CancelAsync()", "testProject": "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ArcForges.ContentSandbox.Tests.csproj", diff --git a/eng/policy/architecture-projects.json b/eng/policy/architecture-projects.json index 22ec1cb2..0ae3693b 100644 --- a/eng/policy/architecture-projects.json +++ b/eng/policy/architecture-projects.json @@ -327,14 +327,6 @@ "production": true, "aot": true }, - { - "path": "src/Native/ArcForges.Native.Pdf/ArcForges.Native.Pdf.csproj", - "role": "NativeAdapter", - "owner": "DesktopPlatform", - "module": "", - "production": true, - "aot": true - }, { "path": "tests/ArchitectureTests/ArcForges.Tests.ArchitectureTests.csproj", "role": "Test", diff --git a/eng/policy/dependency-policy.json b/eng/policy/dependency-policy.json index 3d701acd..2c147850 100644 --- a/eng/policy/dependency-policy.json +++ b/eng/policy/dependency-policy.json @@ -255,12 +255,12 @@ "src/DesktopHelpers/ArcForges.ContentSandbox.Broker/packages.lock.json": "d484757d0de8539423641c61aea8b03bbc681b39826035d3a550326205779f07", "src/DesktopHelpers/ArcForges.ContentSandbox.Contracts/ArcForges.ContentSandbox.Contracts.csproj": "9cf9cdf2e72c9c55da5b9fda99a2efc07d7d918738ca89ae24456d2e762bd660", "src/DesktopHelpers/ArcForges.ContentSandbox.Contracts/packages.lock.json": "fac7c3ce73f75023748acd57900b6fea97e1913991e28811f2c18ffe88c44e36", - "src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj": "566d71ed786f38de30ab92b4886c007b1648a3999ae5c55c176fd77a95eeea54", + "src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj": "c03396c8d83d259ce0ec1eb3e74b9992e1df662a8161ffa8f06abdd98f8d5c1d", "src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/ArcForges.ContentSandbox.HostileFixture.csproj": "c902b6c2d32ec042573459b66d6984cc92a6698e1ef7a5908a0bf98b843ce306", - "src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/packages.lock.json": "238d555c021142cfe486021ed330f4e381295f9984111e23251049fbc50cf755", + "src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/packages.lock.json": "60e75e863ef0bcdd1721a3e50d5b37e498f266e45a3adc8c9511791b3c15a673", "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ArcForges.ContentSandbox.Tests.csproj": "9d97cbef2c2c5585c3698a2c5f7ade78aed10ff806ebc513965a97cada38bc1b", - "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/packages.lock.json": "861aa7f22729ff1f9800431f04eaffbfe3b87a2d21548a98b52f5935c4185e85", - "src/DesktopHelpers/ArcForges.ContentSandbox/packages.lock.json": "71a8114d5743bd3439d49f15803ba675d77664fb08d382344f74f77bbed60cf7", + "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/packages.lock.json": "b0995a525ab6ee3403c30f3857bed38332a518c5a00ef58ced76de21d08e0ac4", + "src/DesktopHelpers/ArcForges.ContentSandbox/packages.lock.json": "1fba3ecd713e869f5f2394823a6121c3a65eff76035256782c4a9302e11b091e", "src/Directory.Build.props": "bf118d90b53ceb536479db4a803b370cd47fc15aa47d98a88902b8ab0dd54047", "src/Native/ArcForges.Native.Abstractions/ArcForges.Native.Abstractions.csproj": "874cb5b91a52e1a3f01adad7819f20c70bdbf8641d4f65c0bc44dcbe70337d0b", "src/Native/ArcForges.Native.Abstractions/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", @@ -268,8 +268,6 @@ "src/Native/ArcForges.Native.Image.Runtime.win-x64/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", "src/Native/ArcForges.Native.Image/ArcForges.Native.Image.csproj": "f40721a408237dd02ae4d0a244731463245a88dfa66fb9e6fe94921090b9e651", "src/Native/ArcForges.Native.Image/packages.lock.json": "cd7ed1d6af3e01a3bc233a62e7ae062e55434f0892d3f61c011387cd9ed0e576", - "src/Native/ArcForges.Native.Pdf/ArcForges.Native.Pdf.csproj": "cc7a8e5ff6f8239f076e04dbdb6fc1f4f61be5219752541758c8f11d3f1e040a", - "src/Native/ArcForges.Native.Pdf/packages.lock.json": "cd7ed1d6af3e01a3bc233a62e7ae062e55434f0892d3f61c011387cd9ed0e576", "tests/ArchitectureTests/ArcForges.Tests.ArchitectureTests.csproj": "fe6d0b2a9bf1cb70fc496515ea35e07356a0758898ba22a3816d696fcebf3c3a", "tests/ArchitectureTests/packages.lock.json": "e2090c8b97e17e0ec5899d8819ac16997a6e0e1df1f3b545c55e5cb39d823168", "tests/AssistantAbstractionsTests/AssistantAbstractionsTests.csproj": "674ea930fead5b76640b5013f9c0348ef175c03184fa8a1382437c7ccd3fa594", @@ -783,19 +781,19 @@ "nativeGate": "eng/native_provenance.py", "sourceGate": "eng/check_provenance.py", "review": { - "owner": "w-c20261005-nat14", - "reviewer": "Pending independent exact-head dependency-admission review before merge.", - "reviewedOn": "2026-10-05", - "baselineCommit": "0e5244a1a9303ea12b6fefddb9c760f6ab424e3a", - "maintenanceAssessment": "NAT.14 adds no third-party dependency. It adds the first-party non-packable project ArcForges.Native.Pdf (a source-generated LibraryImport binding of the owned arc_pdf_* ABI), makes the existing ArcForges.ContentSandbox helper reference it, and adds the first-party native library source native/arcpdf-abi, which links no PDF parser (backend_none: arc_pdf_open fails closed with ARC_UNSUPPORTED). No PDFium source, binary, port, vcpkg change, NuGet coordinate or Python requirement is added; the locked NuGet closure keeps every coordinate, version and content hash, and only the project-reference sections of the ContentSandbox, Fixture and Tests locks and the new Native.Pdf lock change. The one edit outside the task scope is an InternalsVisibleTo(ArcForges.Native.Pdf) line in Native.Abstractions (ADP-07 supporting binding, no behaviour change). The real PDFium dependency and its admission are the separate task NAT.15.", + "owner": "w-deku-20261008-nat-32", + "reviewer": "w-deku-20261008-rev-nat-32 (independent reviewer session named in the NAT.32 review assignment)", + "reviewedOn": "2026-10-08", + "baselineCommit": "f833de3410183d8c59b17ec54229e06ad1370d98", + "maintenanceAssessment": "NAT.32 adds no third-party dependency. It retires the merged PDF engine (P2-022): it removes the first-party non-packable project ArcForges.Native.Pdf (a LibraryImport binding of the owned arc_pdf_* ABI), the first-party native source directory native/arcpdf-abi (backend=none: no PDF parser was linked), the ContentSandbox PDF parser path and the ProjectReference from the ContentSandbox helper, its Fixture and Tests to ArcForges.Native.Pdf. No PDFium source, binary, port, vcpkg change, NuGet coordinate or Python requirement is added or removed. The NuGet closure (60 coordinates, versions and content hashes) and the Python CI closure (10 hash-pinned coordinates) are unchanged. The only dependency-input edits are the three ContentSandbox packages.lock.json files (the project-type entry for ArcForges.Native.Pdf and its dependency edge removed, and the stale project-type entry for ArcForges.Native.Abstractions removed by an unlocked restore under the pinned SDK 10.0.400, because no project in those graphs references it after the retirement), the ContentSandbox project header comment and its ProjectReference; the Native.Pdf project and its lock are deleted. The receipt chains from nat-14-r1, the active receipt on origin/main; nat-14-r1 stays unmodified and immutable history.", "upgradeChecks": { - "compilation": "With the pinned .NET SDK 10.0.400 (separate installed copy at ~/.dotnet) through the workstation build slot: restore regenerated the ContentSandbox, Fixture and Tests locks and created the Native.Pdf lock; the ContentSandbox test project (which builds Native.Pdf, the helper, the fixture and the broker) compiled with warnings as errors and 170 tests ran (161 passed, 9 opt-in OS checks skipped). The native arcpdf-abi library and its two CTest executables compiled with MSVC /W4 /WX through a scratch CMake wrapper (the repository CMake preset needs the unavailable pinned vcpkg tree) and both CTest targets passed, and the engine test also passed unmodified under MSVC AddressSanitizer. Linux and macOS compilation of the native sources was not run locally.", - "aot-trim": "Native.Pdf uses source-generated LibraryImport with DisableRuntimeMarshalling, UnmanagedCallersOnly callbacks and Utf8JsonReader; no reflection, no runtime code generation. The local build compiled it with the repository analyzers (IsAotCompatible is not set on this non-packable project; the analyzers of the helper project that references it ran). The helper Native AOT publish is the existing compile-only CI job; it was not run locally.", - "compatibility": "No package identity, public API of a published package or schema changes. Native.Pdf is non-packable and not in eng/packaging/packages.json. Native.Abstractions gains only an InternalsVisibleTo entry. The helper keeps its launch frame, contract and exit codes; the production parser composition now contains one profile and a launch that names the hostile test profile is still refused.", - "licence-provenance": "All new files are first-party AGPL-3.0-only with SPDX headers, registered in the architecture, licence-boundary, runtime-ownership, active-project and first-party provenance inventories. No upstream source, notice or binary is added.", - "security-sbom": "The NuGet and Python closures are unchanged (same coordinates and content hashes), so no SBOM or advisory input changes. The new native library adds no dependency edge. The hostile-input surface added is the helper's PDF adapter and the engine's validation of backend output, covered by offline tests; no real PDF parser exists in this change.", - "runtime-performance-migration": "No runtime behaviour of any published package changes and no migration exists. No real PDF was parsed, no OS-isolation check was re-run against a real parser and no packaged consumer was executed: the only parser behind the engine in tests is a scripted fake. All PDF behaviour is proven only against scripted fakes; the production helper fails closed (no backend, win-x64 loader only).", - "framework-runtime-posture": "No framework, SDK, runtime or toolchain version changes; global.json and the vcpkg baseline are untouched." + "compilation": "Pinned SDK 10.0.400 in the local WSL2 Debian distribution, on a Linux-native copy: dotnet restore DesktopPlatform.slnx --locked-mode passed; dotnet build DesktopPlatform.slnx -c Release --no-restore with TreatWarningsAsErrors passed (0 warnings, 0 errors); the ContentSandbox Tests project passed (98 passed, 9 opt-in operating-system checks skipped). After the lock regeneration (commit 7f2dfa40) the three ContentSandbox projects were restored with --locked-mode, the Tests project built with 0 warnings and 0 errors, and its tests passed again (98 passed, 9 skipped) under SDK 10.0.400 in the same WSL2 copy. Windows: the whole solution built in Release with TreatWarningsAsErrors under SDK 10.0.401 through the workstation build slot (0 errors), using an uncommitted global.json adapter that was reverted before this receipt. clang compiled the C17 and C++20 native ABI layout tests with the edited header, with their static asserts. The native CMake and CTest build was not run locally: the pinned vcpkg tree is not available on this workstation.", + "aot-trim": "The ContentSandbox helper was published with Native AOT for linux-x64 under the pinned SDK 10.0.400 in the Linux-native copy and produced its native binary with no analyzer or AOT warnings. The helper, its Fixture and Tests compile with the repository analyzers under warnings-as-errors on Windows and Linux. No Windows Native AOT publish was run in this change; the hosted compile-only job remains the CI owner of the Windows publish.", + "compatibility": "No package identity, published public API or wire schema changes. The Contracts repository is not edited: sandbox.proto and the local-sandbox-1.0.0-ci.287.1 compatibility pin are unchanged, and the five PDF RPCs stay in the published schema with closed answers. The ContentSandbox helper, Fixture and Tests are non-packable. The common ABI loses only the unpublished arc_pdf_* block; the image and instrument ABI families are unchanged. The NativeAbiLayout managed tests (3) passed on Windows after the 16-entry update.", + "licence-provenance": "First-party AGPL-3.0-only files are removed together with their inventory rows; no upstream source, notice or binary is added or changed, and NOTICE.txt is unchanged (its PDF hit is an unrelated licence file). The new receipt is first-party and listed in the inventory. eng/check_provenance.py for DesktopPlatform passed against origin/main (974 files, 140 records). The retired-artifact registry is not extended by this change: its GOV.17 model admits only P2-019 native package targets with a provenance record, and the PDF material was never packaged.", + "security-sbom": "No dependency closure change: the 60 NuGet coordinates (versions and content hashes) and the 10 Python tool coordinates are identical to nat-14-r1, and an unlocked restore under the pinned SDK 10.0.400 in a Linux-native copy regenerated the three edited ContentSandbox locks. The only difference from the previous locks is removal of the stale arcforges.native.abstractions Project entry: no package entry, version or content hash changed, and the SDK-bundled ILLink and ILCompiler stay at 10.0.11. The regenerated locks then passed --locked-mode restore under the same SDK (commit 7f2dfa40). The earlier claim of byte-identical regeneration was not reproduced and is withdrawn. No new advisory input. The removed native input surface is the PDF adapter and engine, whose only backend was none; no surface is added.", + "runtime-performance-migration": "No migration and no change to any published package runtime behaviour. The five PDF RPCs answer their closed refusals without any parser: five closed-refusal tests pass on Windows and on Linux. No PDF parsing, rendering, preview or AI PDF reading is retained or claimed. No operating-system isolation check was re-run; the opt-in OS checks stay skipped.", + "framework-runtime-posture": "No framework, SDK, runtime or toolchain pin changes; global.json stays at 10.0.400 with rollForward disabled. The Windows 10.0.401 build used an uncommitted adapter only; that SDK bundles Microsoft.NET.ILLink.Tasks 10.0.12 against the 10.0.11 lock, so the Windows build is not evidence for the locks, which were verified under 10.0.400 on Linux." }, "inputHashes": { ".python-version": "922fe0c3de073b01988e23348ea184456161678c5e329e6f34be89be24383f93", @@ -1051,12 +1049,12 @@ "src/DesktopHelpers/ArcForges.ContentSandbox.Broker/packages.lock.json": "d484757d0de8539423641c61aea8b03bbc681b39826035d3a550326205779f07", "src/DesktopHelpers/ArcForges.ContentSandbox.Contracts/ArcForges.ContentSandbox.Contracts.csproj": "9cf9cdf2e72c9c55da5b9fda99a2efc07d7d918738ca89ae24456d2e762bd660", "src/DesktopHelpers/ArcForges.ContentSandbox.Contracts/packages.lock.json": "fac7c3ce73f75023748acd57900b6fea97e1913991e28811f2c18ffe88c44e36", - "src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj": "566d71ed786f38de30ab92b4886c007b1648a3999ae5c55c176fd77a95eeea54", + "src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj": "c03396c8d83d259ce0ec1eb3e74b9992e1df662a8161ffa8f06abdd98f8d5c1d", "src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/ArcForges.ContentSandbox.HostileFixture.csproj": "c902b6c2d32ec042573459b66d6984cc92a6698e1ef7a5908a0bf98b843ce306", - "src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/packages.lock.json": "238d555c021142cfe486021ed330f4e381295f9984111e23251049fbc50cf755", + "src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/packages.lock.json": "60e75e863ef0bcdd1721a3e50d5b37e498f266e45a3adc8c9511791b3c15a673", "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ArcForges.ContentSandbox.Tests.csproj": "9d97cbef2c2c5585c3698a2c5f7ade78aed10ff806ebc513965a97cada38bc1b", - "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/packages.lock.json": "861aa7f22729ff1f9800431f04eaffbfe3b87a2d21548a98b52f5935c4185e85", - "src/DesktopHelpers/ArcForges.ContentSandbox/packages.lock.json": "71a8114d5743bd3439d49f15803ba675d77664fb08d382344f74f77bbed60cf7", + "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/packages.lock.json": "b0995a525ab6ee3403c30f3857bed38332a518c5a00ef58ced76de21d08e0ac4", + "src/DesktopHelpers/ArcForges.ContentSandbox/packages.lock.json": "1fba3ecd713e869f5f2394823a6121c3a65eff76035256782c4a9302e11b091e", "src/Directory.Build.props": "bf118d90b53ceb536479db4a803b370cd47fc15aa47d98a88902b8ab0dd54047", "src/Native/ArcForges.Native.Abstractions/ArcForges.Native.Abstractions.csproj": "874cb5b91a52e1a3f01adad7819f20c70bdbf8641d4f65c0bc44dcbe70337d0b", "src/Native/ArcForges.Native.Abstractions/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", @@ -1064,8 +1062,6 @@ "src/Native/ArcForges.Native.Image.Runtime.win-x64/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", "src/Native/ArcForges.Native.Image/ArcForges.Native.Image.csproj": "f40721a408237dd02ae4d0a244731463245a88dfa66fb9e6fe94921090b9e651", "src/Native/ArcForges.Native.Image/packages.lock.json": "cd7ed1d6af3e01a3bc233a62e7ae062e55434f0892d3f61c011387cd9ed0e576", - "src/Native/ArcForges.Native.Pdf/ArcForges.Native.Pdf.csproj": "cc7a8e5ff6f8239f076e04dbdb6fc1f4f61be5219752541758c8f11d3f1e040a", - "src/Native/ArcForges.Native.Pdf/packages.lock.json": "cd7ed1d6af3e01a3bc233a62e7ae062e55434f0892d3f61c011387cd9ed0e576", "tests/ArchitectureTests/ArcForges.Tests.ArchitectureTests.csproj": "fe6d0b2a9bf1cb70fc496515ea35e07356a0758898ba22a3816d696fcebf3c3a", "tests/ArchitectureTests/packages.lock.json": "e2090c8b97e17e0ec5899d8819ac16997a6e0e1df1f3b545c55e5cb39d823168", "tests/AssistantAbstractionsTests/AssistantAbstractionsTests.csproj": "674ea930fead5b76640b5013f9c0348ef175c03184fa8a1382437c7ccd3fa594", @@ -1085,7 +1081,7 @@ "frameworkVersions": { "dotnetSdk": "10.0.400" }, - "previousReceipt": "eng/policy/dependency-reviews/plt-25-r1.json", + "previousReceipt": "eng/policy/dependency-reviews/nat-14-r1.json", "namingToolCandidate": { "package": "ArcForges.Contracts.Validation", "version": "1.0.0-ci.205.1", @@ -1096,7 +1092,7 @@ "publication": "https://github.com/ArcForges/Contracts/actions/runs/36356146302" } }, - "reviewReceipt": "eng/policy/dependency-reviews/nat-14-r1.json", + "reviewReceipt": "eng/policy/dependency-reviews/nat-32-r1.json", "pythonClosure": { "pre-commit/4.6.0": { "hashes": [ diff --git a/eng/policy/dependency-reviews/nat-32-r1.json b/eng/policy/dependency-reviews/nat-32-r1.json new file mode 100644 index 00000000..92e1ab6e --- /dev/null +++ b/eng/policy/dependency-reviews/nat-32-r1.json @@ -0,0 +1,1049 @@ +{ + "review": { + "owner": "w-deku-20261008-nat-32", + "reviewer": "w-deku-20261008-rev-nat-32 (independent reviewer session named in the NAT.32 review assignment)", + "reviewedOn": "2026-10-08", + "baselineCommit": "f833de3410183d8c59b17ec54229e06ad1370d98", + "maintenanceAssessment": "NAT.32 adds no third-party dependency. It retires the merged PDF engine (P2-022): it removes the first-party non-packable project ArcForges.Native.Pdf (a LibraryImport binding of the owned arc_pdf_* ABI), the first-party native source directory native/arcpdf-abi (backend=none: no PDF parser was linked), the ContentSandbox PDF parser path and the ProjectReference from the ContentSandbox helper, its Fixture and Tests to ArcForges.Native.Pdf. No PDFium source, binary, port, vcpkg change, NuGet coordinate or Python requirement is added or removed. The NuGet closure (60 coordinates, versions and content hashes) and the Python CI closure (10 hash-pinned coordinates) are unchanged. The only dependency-input edits are the three ContentSandbox packages.lock.json files (the project-type entry for ArcForges.Native.Pdf and its dependency edge removed, and the stale project-type entry for ArcForges.Native.Abstractions removed by an unlocked restore under the pinned SDK 10.0.400, because no project in those graphs references it after the retirement), the ContentSandbox project header comment and its ProjectReference; the Native.Pdf project and its lock are deleted. The receipt chains from nat-14-r1, the active receipt on origin/main; nat-14-r1 stays unmodified and immutable history.", + "upgradeChecks": { + "compilation": "Pinned SDK 10.0.400 in the local WSL2 Debian distribution, on a Linux-native copy: dotnet restore DesktopPlatform.slnx --locked-mode passed; dotnet build DesktopPlatform.slnx -c Release --no-restore with TreatWarningsAsErrors passed (0 warnings, 0 errors); the ContentSandbox Tests project passed (98 passed, 9 opt-in operating-system checks skipped). After the lock regeneration (commit 7f2dfa40) the three ContentSandbox projects were restored with --locked-mode, the Tests project built with 0 warnings and 0 errors, and its tests passed again (98 passed, 9 skipped) under SDK 10.0.400 in the same WSL2 copy. Windows: the whole solution built in Release with TreatWarningsAsErrors under SDK 10.0.401 through the workstation build slot (0 errors), using an uncommitted global.json adapter that was reverted before this receipt. clang compiled the C17 and C++20 native ABI layout tests with the edited header, with their static asserts. The native CMake and CTest build was not run locally: the pinned vcpkg tree is not available on this workstation.", + "aot-trim": "The ContentSandbox helper was published with Native AOT for linux-x64 under the pinned SDK 10.0.400 in the Linux-native copy and produced its native binary with no analyzer or AOT warnings. The helper, its Fixture and Tests compile with the repository analyzers under warnings-as-errors on Windows and Linux. No Windows Native AOT publish was run in this change; the hosted compile-only job remains the CI owner of the Windows publish.", + "compatibility": "No package identity, published public API or wire schema changes. The Contracts repository is not edited: sandbox.proto and the local-sandbox-1.0.0-ci.287.1 compatibility pin are unchanged, and the five PDF RPCs stay in the published schema with closed answers. The ContentSandbox helper, Fixture and Tests are non-packable. The common ABI loses only the unpublished arc_pdf_* block; the image and instrument ABI families are unchanged. The NativeAbiLayout managed tests (3) passed on Windows after the 16-entry update.", + "licence-provenance": "First-party AGPL-3.0-only files are removed together with their inventory rows; no upstream source, notice or binary is added or changed, and NOTICE.txt is unchanged (its PDF hit is an unrelated licence file). The new receipt is first-party and listed in the inventory. eng/check_provenance.py for DesktopPlatform passed against origin/main (974 files, 140 records). The retired-artifact registry is not extended by this change: its GOV.17 model admits only P2-019 native package targets with a provenance record, and the PDF material was never packaged.", + "security-sbom": "No dependency closure change: the 60 NuGet coordinates (versions and content hashes) and the 10 Python tool coordinates are identical to nat-14-r1, and an unlocked restore under the pinned SDK 10.0.400 in a Linux-native copy regenerated the three edited ContentSandbox locks. The only difference from the previous locks is removal of the stale arcforges.native.abstractions Project entry: no package entry, version or content hash changed, and the SDK-bundled ILLink and ILCompiler stay at 10.0.11. The regenerated locks then passed --locked-mode restore under the same SDK (commit 7f2dfa40). The earlier claim of byte-identical regeneration was not reproduced and is withdrawn. No new advisory input. The removed native input surface is the PDF adapter and engine, whose only backend was none; no surface is added.", + "runtime-performance-migration": "No migration and no change to any published package runtime behaviour. The five PDF RPCs answer their closed refusals without any parser: five closed-refusal tests pass on Windows and on Linux. No PDF parsing, rendering, preview or AI PDF reading is retained or claimed. No operating-system isolation check was re-run; the opt-in OS checks stay skipped.", + "framework-runtime-posture": "No framework, SDK, runtime or toolchain pin changes; global.json stays at 10.0.400 with rollForward disabled. The Windows 10.0.401 build used an uncommitted adapter only; that SDK bundles Microsoft.NET.ILLink.Tasks 10.0.12 against the 10.0.11 lock, so the Windows build is not evidence for the locks, which were verified under 10.0.400 on Linux." + }, + "inputHashes": { + ".python-version": "922fe0c3de073b01988e23348ea184456161678c5e329e6f34be89be24383f93", + "Directory.Build.props": "5755571e24d77ac71c2acee2d14e511b308269d84fc8b01ef6be19cdf08fa418", + "Directory.Build.targets": "f38a795efa407c99b0190316cbfece23d0e84d179b5ec63ba88257746412a917", + "Directory.Packages.props": "14603721322414e9d7cb8f5ba847d4539deecdb35e8ebda94fbf68317575dea8", + "NuGet.config": "274d3bbf1913f74ace4d77a96223ea744018536aca462b58b6283ac94cfc2f07", + "benchmarks/probes/acquisition/AcquisitionProbe.csproj": "02bf424b1c64762d0303d4a24a8715a3c2123ac60e3aac146c14be245b5d44bd", + "benchmarks/probes/acquisition/packages.lock.json": "cbbc653c41d6c060a74f249099ac50c04062869ec4bd91000fe234164f04b7e6", + "eng/acceptance/foundation/Directory.Packages.props": "6c073bc8da6fad53cdc35adb042590e2e6b8cb4fbca8e3e1c2ea799476021ad4", + "eng/acceptance/foundation/Foundation.Acceptance.csproj": "df9c2b1ef09c4ffe7473c9a89ed0cccf8a5bfc009a38c81c0899da7ca06ff65a", + "eng/acceptance/foundation/package-lock.json": "2caa7295ba1f8ecd23890a8c8671f21412a186f5c91f8117ef428f2a918c91b7", + "eng/acceptance/foundation/package.json": "32361db70e2072b195385c9141b7bd5b003fd51f6a919b566fcb2c152da8b780", + "eng/acceptance/foundation/packages.lock.json": "36a2e2d446d5bf69b04398db18abcaa73d7c08855b046107cc2136695e2de27c", + "eng/build/desktop-aot.props": "eda4a383b69a800f73e7bb4572e0695d33012ff275c4f27436caea2b11d276ef", + "eng/build/desktop-rids.props": "83f9c4d73353858171cf959b773cfb833afe08f522ce44356dcd27f8aeb2e357", + "eng/native/vcpkg/system-dependencies.v1.json": "54049f3a631bd9388181675ffaaec55be4157dfa3f9288ebe037c068581baa31", + "eng/native/vcpkg/triplets/x64-windows-static-md.cmake": "6935956ac45214d00f49e0eec2b33ef85495b9c2e91ec0f27d5e5225a904320f", + "eng/native/vcpkg/triplets/x64-windows.cmake": "1f0d1876aafd0730d364461ff5e3d60398404c70a237e57d50f879e44208125a", + "eng/packaging/packages.json": "6cb689cac903c5c6065f8cc5c1163547e33bc68c7d8cac884f023651ae207907", + "eng/policy/naming-package.json": "5324d0cc765a48a8b3cee0ab83242156a5ed3e6d34bd6df7e29011f269f7f727", + "eng/provenance/artifact-profiles/native-win-x64-r1.json": "1c708739c47c84c6684665696c0411e4b9055898294ef52bd0798e7fba9b3032", + "eng/provenance/artifact-profiles/native-win-x64-r2.json": "2469bb7ff266028a7c072522d12beb97f6fe11d44a109b1c31932a541657e10d", + "eng/provenance/artifact-profiles/native-win-x64-r3.json": "4663b812996a60f91c5620c1cd58675203a870bdf28001f60cef7a67ae585775", + "eng/provenance/artifact-profiles/native-win-x64-r4.json": "1cf40d8075ae94ca095607fb60cb5ea2cb9ed42f9008c92a6e643660c1756589", + "eng/provenance/records/canonical-agpl-legal-r1.json": "876a4d67f4f1cdba5e43a2431f73a242d3e34fa81b45620b8d45a76781d23dd9", + "eng/provenance/records/contracts-apache-legal-r1.json": "d436592a4d2f88a0de71a04bcf1c7f52cdf313049ef1b713e525a3ef57025b59", + "eng/provenance/records/contracts-provenance-tools-r1.json": "734af233a171876f4f246e0d7f99c7bc5366be65da9801acbd2a1f5a7588bfce", + "eng/provenance/records/contracts-provenance-tools-r2.json": "c83a17cc144e82979a35e0a66cadc1577acfe65aa557c08aa9142a54ac6113d2", + "eng/provenance/records/design-policy-export-r1.json": "8146b3e232890bc712de566049e672c0f6d783c86b4afb04052a130e4ceaeb06", + "eng/provenance/records/design-policy-export-r2.json": "f56cf25acb6448ca7b7ad53b258e7ed7c5a5d43fc7e7bf142c55c4c2fe4fbe29", + "eng/provenance/records/design-policy-export-r3.json": "e1133747e9d6a43f4b6d35c04f91ad1522a8a1d47d9f64e47b44359567d59799", + "eng/provenance/records/design-policy-export-r4.json": "9dbd02e1c0f8466928dc5dfd4bfecf4f25ee04c5d138d51e125ac71cc8f79300", + "eng/provenance/records/native-amd-amf-r1.json": "906e812cba398a382dd9c4788fcad5361bca9704fdd07fe6d0d1358f9d08f3de", + "eng/provenance/records/native-amd-amf-r2.json": "07f5d3fa3f6003f1b4360315cd9becfcfa68bdf3df53c0191bdf65d85660caf9", + "eng/provenance/records/native-amd-amf-r3.json": "b79e83311b0dcc6812fbe4c960c9e8e836e198c375208ffc8c900aecb0ced9d1", + "eng/provenance/records/native-bzip2-r1.json": "ccd1913cbe0f61b1e5bc67b5099a5ebb3f431bc351f0b22195ed5042aec422e4", + "eng/provenance/records/native-bzip2-r2.json": "3252bf8b054c8c036bdb6c6ed0935a55fad71729b14d9fc6a9c09b51e6e564a5", + "eng/provenance/records/native-bzip2-r3.json": "2cdfe7772e8a31d5eb0e9fea9f57313500beac73873d28872d2a5301e198db69", + "eng/provenance/records/native-bzip2-r4.json": "1b963de638e07697ded680d62f9c69d5ac357a95e36fd3ee4c6ee8d7d01168f1", + "eng/provenance/records/native-expat-r1.json": "a4fb403414b2e808708729356a2fdeafdd6bb9d7bb017546d02ab3f61438d97a", + "eng/provenance/records/native-expat-r2.json": "d65ee576bf2f991ad930763c39a378bf4b74e4fe00200010c4a4f483f8c98e97", + "eng/provenance/records/native-expat-r3.json": "01a11f605a4a082c91ad6866c849a0629078dea18affc840fe11fca1fa51eb94", + "eng/provenance/records/native-expat-r4.json": "a479f09846fd7d2f5735a405e63f716ec3125430c18b6afc48e6482fd359b2e9", + "eng/provenance/records/native-ffmpeg-bin2c-r1.json": "b62c66155d964d01040c5258747b8774ef7f2fec24400f4a2ba62d4581353a38", + "eng/provenance/records/native-ffmpeg-bin2c-r2.json": "c0bf5ef98c0890d10779eb6698f453b3606f54053c09a272316b5eb620c34425", + "eng/provenance/records/native-ffmpeg-bin2c-r3.json": "a829f636e5a8819c28dbbd366c64dc16d43f7fd577109fc68773ad74dac40eaf", + "eng/provenance/records/native-ffmpeg-r1.json": "9f829be583d78dd1765e9bc5a66e4fc3ce11d90093324847c0ccfd2d94c9c619", + "eng/provenance/records/native-ffmpeg-r2.json": "4c0f8ba025a34afaaaa75d88357dd3d394bcaa0b1fd2f7591adc1a15454ddfbf", + "eng/provenance/records/native-ffmpeg-r3.json": "40266121cd9f30e074b457fcdf107f8936edb64700bbd6bb63f8bf06a2a8cbee", + "eng/provenance/records/native-ffnvcodec-r1.json": "ed29951ba6f59d6a3fb0b479dc160cfe41b390dca047b40209e7179b60b21cf0", + "eng/provenance/records/native-ffnvcodec-r2.json": "56004e44566872a71808c9077f33729bc3db863c3560bb5534293733cd28792a", + "eng/provenance/records/native-ffnvcodec-r3.json": "a34e91ec0113ef0d1558c1b68c92870abc74382b461d1f1ebab59c857a511cfc", + "eng/provenance/records/native-fmt-r1.json": "aad9dc5f7f06294883a531749dd126d714a49c61c499b5de22448a9e6318b796", + "eng/provenance/records/native-fmt-r2.json": "fe2717b9b3d9577973f9d58ed4e4136fa05ea13bdefe7acb2a202a04eefe74a7", + "eng/provenance/records/native-fmt-r3.json": "0fb8b32a4e04fc4f6cc7b435f33745c4048c200b01b6778bd4c81c8a77de8098", + "eng/provenance/records/native-fmt-r4.json": "48a194a6aa0d710c0d42fdce2e9c878584e2a9ab6b83142c5a70b4e9edbb068a", + "eng/provenance/records/native-imath-r1.json": "9c75fb08b77d704d4a02d1f0dc42c1b9faf9987b152fce8349256991c14dbca0", + "eng/provenance/records/native-imath-r2.json": "fa154ba424b28147ae658266e9e619c0aa3ce1fa8caf62b9d4adcd30d1913bc0", + "eng/provenance/records/native-imath-r3.json": "7d20bda714f8f2a4e4941060cc04306f3d2623462217d26830ad55dc53826de3", + "eng/provenance/records/native-imath-r4.json": "9f33784a7544ed336b1048fefecb385755f981dc04679dfa0ab116d8387f739c", + "eng/provenance/records/native-libdeflate-r1.json": "6497100993a2d241d1f5a853ef0dc06bceb1e2e34a8139c70661a1505f8cabcf", + "eng/provenance/records/native-libdeflate-r2.json": "eb5d80799ed240163c2e2e8189dee9f743125b17d9fc239221ea7f7b4de65c3c", + "eng/provenance/records/native-libdeflate-r3.json": "669c68620aaba0205eb6716e8bd275f2daad62dbe68a3304055cb9c34d5eaa31", + "eng/provenance/records/native-libdeflate-r4.json": "4882ca6c915b23d0f5c7e060c231aae872f2aea28aa138282afec4cff66970d6", + "eng/provenance/records/native-libjpeg-turbo-r1.json": "32a61377fd43596b779f712b51562a151faa80b528555b50632047e66851810d", + "eng/provenance/records/native-libjpeg-turbo-r2.json": "6ddb760641dbb1f9798f89751f2aa5bda342ce7923b6534fcf3f2c22d21c36e0", + "eng/provenance/records/native-libjpeg-turbo-r3.json": "a6064580570926734b13f0a4d60aebb75bf5fb42f6d5713964996eae0de3d54f", + "eng/provenance/records/native-libjpeg-turbo-r4.json": "acaa0a7e1e840f7d8510437747727ca12ffcd966b3aa1c2d4997ba90383d3447", + "eng/provenance/records/native-liblzma-r1.json": "fd0c1a82bddc7c399f87ae3fc7f6a37c752a717a8de8209cf29dd2681f4a1a08", + "eng/provenance/records/native-liblzma-r2.json": "1a730b5fdeb98996507972e0e5fd9bac8ce0f1834a4821781ff8557c911bfe2f", + "eng/provenance/records/native-liblzma-r3.json": "9b3cdcc9d491ce667d1d998cfa1342fc62848b0d1a0f7a9dd0ea65c952216f6d", + "eng/provenance/records/native-liblzma-r4.json": "0d3229f7b3659b2b78ef02a4ec3e06ffbc773c2686684da782e2e15482d8e5ef", + "eng/provenance/records/native-libpng-r1.json": "94026969444009ec7c6ca4b8434b0388d76d9adc798e8aa64a0c1c76c312ce58", + "eng/provenance/records/native-libpng-r2.json": "49aa9cb729aa12b9f020306d354b7cca8f01e7cb99f6fb9d6f3c05e497e9a1cf", + "eng/provenance/records/native-libpng-r3.json": "8c4d16283d49c7633a36ff3e692d6ca9f2cb5998a82449e544e7b048cb1210fd", + "eng/provenance/records/native-libpng-r4.json": "b982a80e4ae7f6039674e7e37c6ca74055ac1e020f431191260ea6b8d19800c3", + "eng/provenance/records/native-libusb-r1.json": "9892918e3a2226df2b3826402228096d7793281794547497425dda07a2fa2468", + "eng/provenance/records/native-libusb-r2.json": "050058749be0db4519746165dfcba9b551a68f78c32c0714bf532a6a6769071d", + "eng/provenance/records/native-libusb-r3.json": "74c3aa1e2db6a79a091bb385689b63ff81e52031d15c1c656b700819f42c1df0", + "eng/provenance/records/native-libvpl-r1.json": "7892c43d72e2bd6ab6bfb474bf9d0e0067715783e4e9dc9d53861e872df1bc32", + "eng/provenance/records/native-libvpl-r2.json": "2f98d16c5dfc1c529a546138150ab0914fdbf5bd5be84ca7ae90e02ea03ac8d8", + "eng/provenance/records/native-libvpl-r3.json": "2ed43feaaf7bf4dcb2b2839c1b852b6735db0698f64fbbe9cd13b5506d6d9432", + "eng/provenance/records/native-miniaudio-r1.json": "8177aa80a28da91a83e1fff09c9e4988182dd19e99c0b5b27352a237ee114892", + "eng/provenance/records/native-miniaudio-r2.json": "c08db68f0e152462419d4cb6cc230c0ae435d676441c2952e6f6b4138bc8c066", + "eng/provenance/records/native-miniaudio-r3.json": "7578a40ae8d52de26b8089ec89cefe35166b3b674f6757e722f79e48449c3a59", + "eng/provenance/records/native-minizip-ng-r1.json": "11ecbd550ecdc36687727b0aeca764c9717e5c03d8c64a235e07319da5830045", + "eng/provenance/records/native-minizip-ng-r2.json": "0caccfd1def5804e25957e78c878d5ba2106e895d5937fb676bd4c3c11b15c60", + "eng/provenance/records/native-minizip-ng-r3.json": "39a5a719bf2056d358b06a6707d137775bde40ec45e3693b567d50a4349c0ae0", + "eng/provenance/records/native-minizip-ng-r4.json": "2b724d8b7660f0ec0836ff98cb3947e238c4a6dbb6d5ba0ecbecf53e9f4e5948", + "eng/provenance/records/native-opencolorio-r1.json": "138eb9210518c5b8f64016559d33cdbbcc4925f3d65a86253166f98a11b53bb1", + "eng/provenance/records/native-opencolorio-r2.json": "b674c499bb0e8751337cfa454e81cf65a5cc3e535ea6b4830674446aed7562f3", + "eng/provenance/records/native-opencolorio-r3.json": "0fbeb25ba5df2da0eb60078803737141ceb69d3c115ffc07de890c370cf3c41b", + "eng/provenance/records/native-opencolorio-r4.json": "bee01300d717e136a69d0beac3b8f7fda1c23fdb0c0f26180d1ca04ec77b2b3b", + "eng/provenance/records/native-openexr-r1.json": "591e426761379fcbbf762633f3c4b13a923222d20b1da8528651120950877580", + "eng/provenance/records/native-openexr-r2.json": "681453d9597b9f6d4d9d062133609b464d7dbfdc05ca6d07cf0e5f5766f50395", + "eng/provenance/records/native-openexr-r3.json": "3922bff104cc175dac6555569f4480b1f05fb10441ee8de21363b8a01da678e2", + "eng/provenance/records/native-openexr-r4.json": "3499e79923acd345db4f6bcc38b6a479d476baadb939e9bc293956941a0ae09f", + "eng/provenance/records/native-openimageio-r1.json": "7eff2e066559b33257701d1880f74efb9a556978dc7dc8b5ee1dc9be76a8da9e", + "eng/provenance/records/native-openimageio-r2.json": "2d860bbe419719a72dd96e046141e97e26487cc25886cd02575ce10df4bb8c6d", + "eng/provenance/records/native-openimageio-r3.json": "aa1c8be91b17a1e0b1ffb0530d1d23f71d7faee6e195bcc26f8dae378f574dd0", + "eng/provenance/records/native-openimageio-r4.json": "7eb9f10547a390f60aa1cbcc22a69654bdc713c7af9eb87cc0b9e7c995cb5e8a", + "eng/provenance/records/native-openjph-r1.json": "79779e9a031d2811f1d13830d35f3153cff511f6cb9aa86c28d6a5d905d60079", + "eng/provenance/records/native-openjph-r2.json": "9554e5d133939dd9cdd3cf3df98bff1bdcffa25344fd2ba2633af67bcbc5239d", + "eng/provenance/records/native-openjph-r3.json": "501ed6d3645a8661cabfce92f0bdc411b2bdaa9492a7fc6773f7e45cd3d0cbb1", + "eng/provenance/records/native-openjph-r4.json": "2956db9c112c71584da4ddf8df7fccc61b5b540a6ccbe4ce91269953cb1c73d3", + "eng/provenance/records/native-opentimelineio-r1.json": "72074bcf82b6e813c3aaf4f8fe741498cd0865089f96720f9a832559982f5f0b", + "eng/provenance/records/native-opentimelineio-r2.json": "747303a95d89615a8080d45dc98b1faef71e081fa467c74fd98acca847e00671", + "eng/provenance/records/native-opentimelineio-r3.json": "3fa020da888e7e7942b1eb528d534018159e4d0befc2a7d4505ad04fd352f3b3", + "eng/provenance/records/native-pkgconf-r1.json": "92fefa78ecc8a7cb6c5fb44308800f2d231e49520b92b931e5964ac193aaa7c1", + "eng/provenance/records/native-pkgconf-r2.json": "19801f80373877c21932d2ff5dd506193464f2b21d21ec2d821ec6f0e27246b7", + "eng/provenance/records/native-pkgconf-r3.json": "9f91d9226843ada3def1eeab75f9d4489dfed944dfa2845ca048707925eadc95", + "eng/provenance/records/native-pystring-r1.json": "e519aca475db1028a01c040612b0fd939b92f89c0c00ae163f014d1b382f0db0", + "eng/provenance/records/native-pystring-r2.json": "c0ca0d1783fc12a813ad87867d65ec6b746c15d2502dd8e4f711d67a59e608a6", + "eng/provenance/records/native-pystring-r3.json": "c3a95bd9e99c690f16894e1bc87508495b01a16e19b8f5d501163e5ce11be43f", + "eng/provenance/records/native-pystring-r4.json": "7deba0b0bb547dda8363e1baab8b50040953fffb3c37dcd9b50cee2678931d98", + "eng/provenance/records/native-rapidjson-r1.json": "7c774b26ec3c50f988c507a68a77c3f48782144d91e71f69a5b510b764068b22", + "eng/provenance/records/native-rapidjson-r2.json": "5c8f775457759fece260a38d777e9e69998ea97a57b11d66327b3c0eab17d822", + "eng/provenance/records/native-rapidjson-r3.json": "2cc34eb7e0023cabd4ea674e22b4ddd69155b1fd7eb0809de9bfa34cb707a2db", + "eng/provenance/records/native-robin-map-r1.json": "2358156c1d3e4ba5ec70736b189006fde32d2ea3b20e06c3c55ccc0990f4bb18", + "eng/provenance/records/native-robin-map-r2.json": "78ce58ff330d99f2d931f5ef4884df07fd4a4ff9c08cb88ca33725bb98260391", + "eng/provenance/records/native-robin-map-r3.json": "b05c11319fbff03ccef6bf5c8fdb53a685415d5be4a007afff30a4470f3893c7", + "eng/provenance/records/native-robin-map-r4.json": "455cba8974902c464dcbf352bfbd660c91af54d9798d61f26900d61971aaae0c", + "eng/provenance/records/native-tiff-r1.json": "0ee90f1c9f5bf69f5100537e8b8c42738635836fd9378803eaaa8828505e0c70", + "eng/provenance/records/native-tiff-r2.json": "97b5a80217fae7f0e8e65d5c74e96c1933a6eae793462feb0227d04fe866f93b", + "eng/provenance/records/native-tiff-r3.json": "25917b5ffaa9305c3543f833153be313b53ddeebfa02b0451552f6fed478e597", + "eng/provenance/records/native-tiff-r4.json": "c0de6d50f3ada630a8f9e961b28f4ca03f52bb41542ba6f3343204c211a19953", + "eng/provenance/records/native-vcpkg-cmake-config-r1.json": "26ea3210bcf7e2999f6f1095757dc95d988a9250032be28395e34f0d6ab599db", + "eng/provenance/records/native-vcpkg-cmake-config-r2.json": "9579380b5378c5f6089823ca4e50c1e7117d8496e2bc440c7b2393b5b24d9062", + "eng/provenance/records/native-vcpkg-cmake-config-r3.json": "bf6d10434787c3a5650eddb78f197e3ff4613f991f95de113c92f88c274096d3", + "eng/provenance/records/native-vcpkg-cmake-config-r4.json": "11c5aaa3d5838fe60cf5303912f85dc3e2adff5bb235459e27cc81252074f2aa", + "eng/provenance/records/native-vcpkg-cmake-get-vars-r1.json": "71b698733503d4e18b415ea18fc455540cdc312f0ed4d7ee11ab8dd74cbe7cee", + "eng/provenance/records/native-vcpkg-cmake-get-vars-r2.json": "ec2ff3a4ea0f4a463a62f520e5bd731a790d1ced5c8b7510b19cffeb5280a836", + "eng/provenance/records/native-vcpkg-cmake-get-vars-r3.json": "f38967e2bee6dee771be8d52bc13e4adad16c17bf5300ea7199a83dcb4f6ee30", + "eng/provenance/records/native-vcpkg-cmake-r1.json": "a490a0794543455f34670cc8bd30c264920fabfb4624e800d6193393f5efdf68", + "eng/provenance/records/native-vcpkg-cmake-r2.json": "6c6c7aef911720cec3a134d2290cead6714503705e290c41b7859a52e0f214e2", + "eng/provenance/records/native-vcpkg-cmake-r3.json": "6d480d2873a92fd81ac6bbb42b565d8298da6c7a5f02d4ec8e93231f0449d362", + "eng/provenance/records/native-vcpkg-cmake-r4.json": "3aa9dee30b1c863e48c6c1634de31226ff659f8f41c8861407eb5ba6a71c88b9", + "eng/provenance/records/native-vcpkg-msbuild-r1.json": "1b46af2740931a423f4f0eea0dc1ee13695de9d6f9069c2139df5f5ae4670920", + "eng/provenance/records/native-vcpkg-msbuild-r2.json": "e67619f14992bccef01a67e4865702fa4d4537cafcea06c2fb7bbbfa26a4d938", + "eng/provenance/records/native-vcpkg-msbuild-r3.json": "58c1a2888a05cf7cd1cd4938ef58a4fe12211764df8be606532e23b2dc90ff86", + "eng/provenance/records/native-vcpkg-pkgconfig-get-modules-r1.json": "964dfde1c96acb59a3c3e3ac0dfd9e31aa90d057bf87957189c6fcffe141e1b8", + "eng/provenance/records/native-vcpkg-pkgconfig-get-modules-r2.json": "b1ce3f87b360587156233bafe3ff40cd7544ccd69dda30de878d0f9e790c79e6", + "eng/provenance/records/native-vcpkg-pkgconfig-get-modules-r3.json": "acb1cb2249c13e46a0b2f1b3fb3356a5d116e00b0c7b40133ab414413532245c", + "eng/provenance/records/native-vcpkg-tool-meson-r1.json": "fdef3cb36ad9bd88f67f131a14ce85bf2a09cd83306a4c06843070b14eb6aed4", + "eng/provenance/records/native-vcpkg-tool-meson-r2.json": "2ef534d5c42dc85c60dee2bd095237a06d9f811a8ec79176998bdb89fe5ccc4f", + "eng/provenance/records/native-vcpkg-tool-meson-r3.json": "edf632bc088572ee7178d03ba567e92cb44bc534d4a5f290d263e1b963388b65", + "eng/provenance/records/native-vulkan-headers-r1.json": "0f2593eef072458ff73a3280261129601857e3e5355e13478bf6a99804fe2fc0", + "eng/provenance/records/native-vulkan-headers-r2.json": "89c0592ecf81d4f6be04d59135f24a7f3147abe9d4ce7bdd960efc52aee270cd", + "eng/provenance/records/native-vulkan-headers-r3.json": "816650d83a037ce53749066e5750f1c19432249aa3057fb3c8c21464e49372c4", + "eng/provenance/records/native-yaml-cpp-r1.json": "d1f713849eb1c20d062bbcefbae527f7d6a935be19ffa9c0f8ac76bc5226b376", + "eng/provenance/records/native-yaml-cpp-r2.json": "e0441da6447c9c6e0180f714359c4977b87839bb7095146ffb7ba8b94ba3dab1", + "eng/provenance/records/native-yaml-cpp-r3.json": "57bff8322b7436c303e91a2934e4e64ab8497e6074cfacf2e2a632fd451c459c", + "eng/provenance/records/native-yaml-cpp-r4.json": "6759fa15d23c11c8af5c8b9b8da85c051a347fa2fe6e13263ae932667141585c", + "eng/provenance/records/native-zlib-r1.json": "0c055cc9d8e6be306aa96e9e07c5759f250864b7782682830ff6737ce0303ea4", + "eng/provenance/records/native-zlib-r2.json": "55bb98856e9b931d908caa9dc318e781617959d0bd906c9f36a0911eaff86037", + "eng/provenance/records/native-zlib-r3.json": "15de8fcc0d0afbc23d700fc28158279a86fae71ac0e536ef03472f2a1f92b203", + "eng/provenance/records/native-zlib-r4.json": "5a6473b9f6c01f925d9d53e5f507f07e0b4d0df94e939083137cf6388606662f", + "eng/provenance/records/native-zstd-r1.json": "52ea5f89f1ecdb8b9b4b1c7188a979b45c626d62fec31ba6586e5b44a8e4913b", + "eng/provenance/records/native-zstd-r2.json": "8e0b4aef35606a46f12a785009a11a97c3b795233f6032c08219777ffe7cf127", + "eng/provenance/records/native-zstd-r3.json": "3aee1adfbc2a58523a46023bee145bfac1dd37e5be35f34ba8b072e58d147171", + "eng/provenance/records/native-zstd-r4.json": "3b888e67a3ff11e3d60fe2199d910e2d689b06e5ab6105ea8581b398224ed262", + "eng/provenance/records/otio-legal-r1.json": "b1cafe73055423716f77ce6fa9d85079da10f41ff51b78eaf5ebe12a54dd2b3d", + "eng/provenance/records/otio-python-optional-patch-r1.json": "d666eb12d972e45b1c72ad9fdaa3bb4aa5d3e7475b19728545964b0528354ff1", + "eng/provenance/records/plan-delivery-graph-r1.json": "5715bd29050c9a78dda2db639ea0a7e5deb23037b02f2a10340f6f9ce3ab6287", + "eng/requirements-ci.txt": "fe6a76cb5334fd1b96af6203baec8723653e97453f0649f61b8703fd81787c25", + "eng/verification/probe-evidence/Directory.Packages.props": "970a2681511caf88a41e1128c6240ccbc319ab09fcdfec02e4e53d1963d29ab7", + "eng/verification/probe-evidence/Prf09.TableViewProbe.csproj": "29600888434657600898b01782870d17a2f2c1dee6136cffbbbce802a7beae36", + "eng/verification/probe-evidence/packages.lock.json": "ebaf3ff575a65e6b3a68a661fa19ea987f1abcf901846b80060d2c6ae3afbd02", + "global.json": "d17cfeed29d17cb8e75b56e26767c591ab560a2d7204f28bcb3ec3f47bb4263e", + "native/windows/ArcForges.Native.props": "a3d1e3c574b15b3c428f8b7db98476d120aa276dc566e86a1ade42e910516224", + "src/Build/ArcForges.Build.Policy/ArcForges.Build.Policy.csproj": "8514894388ac30b13550b223dfb94d37d37dbf79816766685e6438ea8aac01da", + "src/Build/ArcForges.Build.Policy/Architecture/ArchitecturePolicy.props": "b49df8103b7251e16412b236bcb2e4a9b665365b3f1b034840a4801653fdeb96", + "src/Build/ArcForges.Build.Policy/build/ArcForges.Build.Metadata.targets": "c0d516a0c0b956689ddfc78ff061d6f3add7a9fe22b4b05baa31bbfed1aca985", + "src/Build/ArcForges.Build.Policy/build/ArcForges.Build.Policy.props": "5310beac8a834ebff477d2d86d2d8937b2f635aa741294225c99a111d8b72600", + "src/Build/ArcForges.Build.Policy/build/ArcForges.Build.Policy.targets": "8c15dc21c5bf599b458e41ea126d2a22157d70f32c81512d25d7f886c504e34e", + "src/Build/ArcForges.Build.Policy/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", + "src/BuildingBlocks/ArcForges.Application.Abstractions/ArcForges.Application.Abstractions.csproj": "270f378987937ebeb8d457ba332d56a8fb0f50a734386402f5358a430db3ebf6", + "src/BuildingBlocks/ArcForges.Application.Abstractions/packages.lock.json": "35787730aacd19510564fce9726ad522b0cea67a7f2380dadaf9bbb0d9bda2fc", + "src/BuildingBlocks/ArcForges.Assistant.Abstractions/ArcForges.Assistant.Abstractions.csproj": "38f313d8eaa527683cbcf2f72eeb78a79fad7a341acf0b10f34cf26159a2859e", + "src/BuildingBlocks/ArcForges.Assistant.Abstractions/packages.lock.json": "33c199ae62f7e0558d6e0b79086a73b6261cceb3c9cc9bbc1f21e9847c426aed", + "src/BuildingBlocks/ArcForges.Capabilities/ArcForges.Capabilities.csproj": "ae05aef953a581e9c2baf98d65d9106145c3edf673779d8ec55daa8ced3ffe7f", + "src/BuildingBlocks/ArcForges.Capabilities/Tests/ArcForges.Capabilities.Tests.csproj": "01ea51cef17b0abe6aa30d7a7cb88c28a619fbd0abc8fb338e6353d5efe961c7", + "src/BuildingBlocks/ArcForges.Capabilities/Tests/packages.lock.json": "b83120656ca0890664744fcf05c3d75dc1c56ccc7a493a118f63c70c7069643b", + "src/BuildingBlocks/ArcForges.Capabilities/packages.lock.json": "35cd295d2a07470c88259af06ca22114c709fa2c4e4b929abd599c96a9c26b0c", + "src/BuildingBlocks/ArcForges.Contributions/ArcForges.Contributions.csproj": "beb382d6fc12a32758741a33ac8d2372360b0dba96536b8edff6f9a9fd9fdec4", + "src/BuildingBlocks/ArcForges.Contributions/Tests/ArcForges.Contributions.Tests.csproj": "fa29f3b8c84f80eef414926739f81f696c7be205b27e0b6da0463f52eace587c", + "src/BuildingBlocks/ArcForges.Contributions/Tests/packages.lock.json": "2329c3088610c295fbaaf35a03c858c17d9b748d10fc733707dd4fde0967de8f", + "src/BuildingBlocks/ArcForges.Contributions/packages.lock.json": "03ee63c6a4335b1c141cef2016983cd9064532846d582ee6fe03dce8ccf866f5", + "src/BuildingBlocks/ArcForges.Desktop.Experience/ArcForges.Desktop.Experience.csproj": "d0620806bcaa6e7770e7617ac1ba671fd6d87d1aee76a7c4f734c9b85f263826", + "src/BuildingBlocks/ArcForges.Desktop.Experience/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", + "src/BuildingBlocks/ArcForges.Desktop.Graphics/ArcForges.Desktop.Graphics.csproj": "2b1f2e7754125794ca2e15fd689455dcc20d4216cc90e4f0109cf7a372c4c528", + "src/BuildingBlocks/ArcForges.Desktop.Graphics/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", + "src/BuildingBlocks/ArcForges.Desktop.Preview/ArcForges.Desktop.Preview.csproj": "dd3771d911a65c24d62a2cffee8d6a72aea18be6817a9d7aa484d14a8ddedddd", + "src/BuildingBlocks/ArcForges.Desktop.Preview/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", + "src/BuildingBlocks/ArcForges.Desktop.RichContent/ArcForges.Desktop.RichContent.csproj": "1c860029517d0882df14a33386c11dee44d1e6fc85adc84e2ef224abf7edf34a", + "src/BuildingBlocks/ArcForges.Desktop.RichContent/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", + "src/BuildingBlocks/ArcForges.Desktop.Text/ArcForges.Desktop.Text.csproj": "fad00ed9768093a4b5ac43855c4cbcd15a9fec6e4b3bdde7a183398b88fbc664", + "src/BuildingBlocks/ArcForges.Desktop.Text/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", + "src/BuildingBlocks/ArcForges.Foundation/ArcForges.Foundation.csproj": "a8eaa0048788cad95cbb73f66b0edad0bab27b312d39f9134aea6e854eb8d2b6", + "src/BuildingBlocks/ArcForges.Foundation/Tests/ArcForges.Foundation.Tests.csproj": "a3dd1928395674cabc3d0a1a88fd2d15c081165620e796767dbe85f5b29d8d1f", + "src/BuildingBlocks/ArcForges.Foundation/Tests/packages.lock.json": "a66aa29dc746ed2999d9cfffd2f3ed75f3e73738e85135eb4df026a58491eb09", + "src/BuildingBlocks/ArcForges.Foundation/packages.lock.json": "275edc2c07cd106e7cafdb851474a56dc233c4326849b290a96ffb2c2cba81c6", + "src/BuildingBlocks/ArcForges.LocalRpc/ArcForges.LocalRpc.csproj": "3422bcba137d44a20048e882ac4fb0f8304ed7b0698330baea939e1371061963", + "src/BuildingBlocks/ArcForges.LocalRpc/Tests/ArcForges.LocalRpc.Tests.csproj": "14e19eb581b42412afe5420aa766b3150839d0bf93a61227457be6665e3ca5be", + "src/BuildingBlocks/ArcForges.LocalRpc/Tests/packages.lock.json": "4526b2979cf62d58566584946bc831f0d32aee9fa8511a4b81faa621fefd2329", + "src/BuildingBlocks/ArcForges.LocalRpc/packages.lock.json": "6fd6cb14bb463d5a940b14742188f214c3b267eff1af3690ee1eea347664f164", + "src/BuildingBlocks/ArcForges.NativeInterop/ArcForges.NativeInterop.csproj": "e62c58cd27cbef29eb9ad0761618fb68fed64ec8e46b03e1e5fd7b57d15c7850", + "src/BuildingBlocks/ArcForges.NativeInterop/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", + "src/BuildingBlocks/ArcForges.Observability.Desktop/ArcForges.Observability.Desktop.csproj": "33f6707a0b23250af70690211ee31ad4ff762bb68ae1b42ef26671a8f41af8a4", + "src/BuildingBlocks/ArcForges.Observability.Desktop/Tests/ArcForges.Observability.Desktop.Tests.csproj": "f530a19cd569d1291e8593a07232a5d30ad41ec966d659f5deb522905a32ebb6", + "src/BuildingBlocks/ArcForges.Observability.Desktop/Tests/packages.lock.json": "041d44c07cced9649ead71a03bf645210c4f7ce105f2667539400dbd85362238", + "src/BuildingBlocks/ArcForges.Observability.Desktop/packages.lock.json": "65a20afa293666592531d349f51b909d3a26327991444dfade78b881d9318252", + "src/BuildingBlocks/ArcForges.Observability/ArcForges.Observability.csproj": "5d3cda9047f57949fd6af419054ae287278533daf02ea825f31c90b9e9af2df0", + "src/BuildingBlocks/ArcForges.Observability/Tests/ArcForges.Observability.Tests.csproj": "e6a49e4f207fcbb3e10fc956a7f4bfbc5c816656dab85f23d28fe2ea1c7cb585", + "src/BuildingBlocks/ArcForges.Observability/Tests/packages.lock.json": "c2bb444ba5d39e0d920b203aa02ccc36512d3d941b9568ddb6a3dc6cb86accd2", + "src/BuildingBlocks/ArcForges.Observability/packages.lock.json": "03ee63c6a4335b1c141cef2016983cd9064532846d582ee6fe03dce8ccf866f5", + "src/BuildingBlocks/ArcForges.Persistence.Derived/ArcForges.Persistence.Derived.csproj": "c811b4409a75567b7140dd928db29cd130b4d3161f0384b5702ec50e8a7950e1", + "src/BuildingBlocks/ArcForges.Persistence.Derived/Tests/ArcForges.Persistence.Derived.Tests.csproj": "9591a3c2c55e3a8ca4ce0b419c9678a37673615eee7226e3074001f0e335c16c", + "src/BuildingBlocks/ArcForges.Persistence.Derived/Tests/packages.lock.json": "59de52cf05cb0fca8c726a6013779f86e239c9499c0ce2b661fd87240a0cc0e7", + "src/BuildingBlocks/ArcForges.Persistence.Derived/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", + "src/BuildingBlocks/ArcForges.Persistence.Resources/ArcForges.Persistence.Resources.csproj": "cef718cfec9d7cf24b15bc0800f1e2553dc2f1f8967b7199e4319759b7c8ff9d", + "src/BuildingBlocks/ArcForges.Persistence.Resources/Tests/ArcForges.Persistence.Resources.Tests.csproj": "3e77a1b68b5009b2393b59c1534d87c9ab26684c850828f03354df89de22d2d7", + "src/BuildingBlocks/ArcForges.Persistence.Resources/Tests/packages.lock.json": "6b3f5df901b107d2f1cc78dca31fe656cb44dffa811be58fb82e26facf5ee112", + "src/BuildingBlocks/ArcForges.Persistence.Resources/packages.lock.json": "35787730aacd19510564fce9726ad522b0cea67a7f2380dadaf9bbb0d9bda2fc", + "src/BuildingBlocks/ArcForges.Persistence.Sqlite/ArcForges.Persistence.Sqlite.csproj": "67f162a18dfcb1c752902e0fd4e8b28c934b8ccbb6636699df3acadb63101f7a", + "src/BuildingBlocks/ArcForges.Persistence.Sqlite/packages.lock.json": "fbb5c962d7866fbf1d129edab2498f277a1fe71afdc0641d3db2e90a671b7408", + "src/BuildingBlocks/ArcForges.Security.Audit/ArcForges.Security.Audit.csproj": "16db2e79a289f9954d871f5d96ff6fef69f9f3a9e05218026b34408f5a4190c7", + "src/BuildingBlocks/ArcForges.Security.Audit/Tests/ArcForges.Security.Audit.Tests.csproj": "e170254575a6b0723efb271ab7c1216f006c7a816677bd5b447cfff258e79082", + "src/BuildingBlocks/ArcForges.Security.Audit/Tests/packages.lock.json": "1c93de4dbc04b8f0a15fef30f352c476b7543ff26f381cbdd348c0803fc092fd", + "src/BuildingBlocks/ArcForges.Security.Audit/packages.lock.json": "ba8caa1127c0dc5f1036d45aea694e80008a6395dbe6c5a1d96e3fc4a2bf4352", + "src/BuildingBlocks/ArcForges.Security.Secrets/ArcForges.Security.Secrets.csproj": "8ff03fdcaf23b09386fac47dd5e3e4f9689a665a569137f726456c9e59a9ecef", + "src/BuildingBlocks/ArcForges.Security.Secrets/Tests/ArcForges.Security.Secrets.Tests.csproj": "d6798bbbcf980fab4467c6d831871b075e55aeef6e09c7df67c4d72cd2334120", + "src/BuildingBlocks/ArcForges.Security.Secrets/Tests/packages.lock.json": "ba013fd952ee15fd79f34bfdd37c1113b5933b94d89d83bdb9373974caee6e62", + "src/BuildingBlocks/ArcForges.Security.Secrets/packages.lock.json": "a70e1701b6e854e19dc5aee64ca29bd9c48b408eb0251d7bb181d4d1bd55e1bd", + "src/BuildingBlocks/ArcForges.Security/ArcForges.Security.csproj": "948f9509206ab6dc95542f773cf62728ce72f54097f0d89028f65b32afed0f70", + "src/BuildingBlocks/ArcForges.Security/CapabilityEnforcement/ArcForges.Security.CapabilityEnforcement.csproj": "13989b4b7f680da049d034c5c21529f6d15042394a9247d480902008370396b4", + "src/BuildingBlocks/ArcForges.Security/CapabilityEnforcement/packages.lock.json": "d853a740c8ddb1da79ae2aad7d54fcecf80ed0a73dac94df14443c29d4eae7f7", + "src/BuildingBlocks/ArcForges.Security/LocalRpcBoundary/ArcForges.Security.LocalRpcBoundary.csproj": "16b8227d01b3a11c82c7111c7da3cbf5d9d8701a3c449c47ca40dd3f245638d9", + "src/BuildingBlocks/ArcForges.Security/LocalRpcBoundary/packages.lock.json": "c9cd093bc0d4fe58c08f1bd0fc591d4f3086c8f302b6a374b319efcc9ec1d3e9", + "src/BuildingBlocks/ArcForges.Security/Tests/ArcForges.Security.Tests.csproj": "0a151cdf32bf378434d9e82659d24df6a8802b52d8163bd97cfefb208236433a", + "src/BuildingBlocks/ArcForges.Security/Tests/packages.lock.json": "790d82612130bcf77c69c55ea91686a1dd63531ad54c422bc6304c2815e40e39", + "src/BuildingBlocks/ArcForges.Security/packages.lock.json": "35787730aacd19510564fce9726ad522b0cea67a7f2380dadaf9bbb0d9bda2fc", + "src/DesignSystem/ArcForges.DesignSystem/ArcForges.DesignSystem.csproj": "c3e379b6f09ee3ddcea9360c742fdd37b45e936e0cb17586714a0a82d9cbb1eb", + "src/DesignSystem/ArcForges.DesignSystem/Tests/ArcForges.DesignSystem.Tests.csproj": "5d5dd7d6656b8e5b78de998771eeaa3158f7a5363cc22538dbb9e19f7b7b656e", + "src/DesignSystem/ArcForges.DesignSystem/Tests/packages.lock.json": "56db8dfaee7c7680201c53073b0243910102e886292a26de735333bb830e8465", + "src/DesignSystem/ArcForges.DesignSystem/packages.lock.json": "687884c0b3e6057ab3732f9f1d042cc81a4cb3b170460a8ac9b140137e436acc", + "src/DesignSystem/ArcForges.Desktop.Shell/ArcForges.Desktop.Shell.csproj": "c0d4c9c68da4779c0d5e6c9e44998f27dc5c4edc0108e396790394c43d5675c3", + "src/DesignSystem/ArcForges.Desktop.Shell/Tests/ArcForges.Desktop.Shell.Tests.csproj": "1ee39b61f57a3740202cfcdd372e6444fbe5eb7494bbc25fadaf1d504b4a94fe", + "src/DesignSystem/ArcForges.Desktop.Shell/Tests/packages.lock.json": "fb8ee728a3353478b0d217bf06125769240b4cc3307b392e80f5c43f61bfd6de", + "src/DesignSystem/ArcForges.Desktop.Shell/packages.lock.json": "a88f5fef8077a6678c68eb12ec52c89cc8449b68fb27190f9b0c92a0768d50b5", + "src/DesktopHelpers/ArcForges.ContentSandbox.Broker/ArcForges.ContentSandbox.Broker.csproj": "9f38eb49ade9e02d64fc9ce8db904f21631d89f7edb1b8c92325130e072d4c50", + "src/DesktopHelpers/ArcForges.ContentSandbox.Broker/packages.lock.json": "d484757d0de8539423641c61aea8b03bbc681b39826035d3a550326205779f07", + "src/DesktopHelpers/ArcForges.ContentSandbox.Contracts/ArcForges.ContentSandbox.Contracts.csproj": "9cf9cdf2e72c9c55da5b9fda99a2efc07d7d918738ca89ae24456d2e762bd660", + "src/DesktopHelpers/ArcForges.ContentSandbox.Contracts/packages.lock.json": "fac7c3ce73f75023748acd57900b6fea97e1913991e28811f2c18ffe88c44e36", + "src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj": "c03396c8d83d259ce0ec1eb3e74b9992e1df662a8161ffa8f06abdd98f8d5c1d", + "src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/ArcForges.ContentSandbox.HostileFixture.csproj": "c902b6c2d32ec042573459b66d6984cc92a6698e1ef7a5908a0bf98b843ce306", + "src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/packages.lock.json": "60e75e863ef0bcdd1721a3e50d5b37e498f266e45a3adc8c9511791b3c15a673", + "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ArcForges.ContentSandbox.Tests.csproj": "9d97cbef2c2c5585c3698a2c5f7ade78aed10ff806ebc513965a97cada38bc1b", + "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/packages.lock.json": "b0995a525ab6ee3403c30f3857bed38332a518c5a00ef58ced76de21d08e0ac4", + "src/DesktopHelpers/ArcForges.ContentSandbox/packages.lock.json": "1fba3ecd713e869f5f2394823a6121c3a65eff76035256782c4a9302e11b091e", + "src/Directory.Build.props": "bf118d90b53ceb536479db4a803b370cd47fc15aa47d98a88902b8ab0dd54047", + "src/Native/ArcForges.Native.Abstractions/ArcForges.Native.Abstractions.csproj": "874cb5b91a52e1a3f01adad7819f20c70bdbf8641d4f65c0bc44dcbe70337d0b", + "src/Native/ArcForges.Native.Abstractions/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", + "src/Native/ArcForges.Native.Image.Runtime.win-x64/ArcForges.Native.Image.Runtime.win-x64.csproj": "e7e89b53d8f01b17385174d0723d480c91159f6209b61eae265bf189fe5645f6", + "src/Native/ArcForges.Native.Image.Runtime.win-x64/packages.lock.json": "09f91faca7c23b7ef4c0a4a6a4afa1f5d4e17e2e3dcbd9aca08ff12aec384059", + "src/Native/ArcForges.Native.Image/ArcForges.Native.Image.csproj": "f40721a408237dd02ae4d0a244731463245a88dfa66fb9e6fe94921090b9e651", + "src/Native/ArcForges.Native.Image/packages.lock.json": "cd7ed1d6af3e01a3bc233a62e7ae062e55434f0892d3f61c011387cd9ed0e576", + "tests/ArchitectureTests/ArcForges.Tests.ArchitectureTests.csproj": "fe6d0b2a9bf1cb70fc496515ea35e07356a0758898ba22a3816d696fcebf3c3a", + "tests/ArchitectureTests/packages.lock.json": "e2090c8b97e17e0ec5899d8819ac16997a6e0e1df1f3b545c55e5cb39d823168", + "tests/AssistantAbstractionsTests/AssistantAbstractionsTests.csproj": "674ea930fead5b76640b5013f9c0348ef175c03184fa8a1382437c7ccd3fa594", + "tests/AssistantAbstractionsTests/packages.lock.json": "b242f814d6724e1c8c5d82e1a969b846f51cb907c9acfd4d426556059785d8c8", + "tests/LocalRpcAotTests/LocalRpcAotTests.csproj": "28e7f64ff9d289bd4943a5e14e36302350ba3ff00d8a36756dae07da38b673f8", + "tests/LocalRpcAotTests/packages.lock.json": "b30ccc15ca94a45952cf840250a88b85d4b4be45d1906fce83234a8d29d0b0c3", + "tests/NativeAbiTests/ArcForges.Tests.NativeAbiTests.csproj": "def3becd3d047014df118bd686a671e55ef7db133c5b03495bd19945b6ebe6e0", + "tests/NativeAbiTests/packages.lock.json": "062e863bfceb7a467a5f004424324c80bb0f3a6f885a9b7de89aa92b90fe6cf1", + "tests/PersistenceTests/ArcForges.Tests.PersistenceTests.csproj": "5a947697cc93979c5e6c149b100fee204d706695e0e4d596af7bf95a674c3008", + "tests/PersistenceTests/packages.lock.json": "c94488ff2560372433bf2ca0d6f04a69429d10cde23265edca24ea6f798b6778", + "tests/ReleaseArtifactTests/GrpcWeb/Directory.Packages.props": "75389a85362198cb44500b21765fda9a7a83ce13a17e6fbeb5b5ec9d9f8b68c7", + "tests/ReleaseArtifactTests/GrpcWeb/GrpcWebAotProbe.csproj": "51a16e4d5b8f0370359b0eaed5677866f656e6dab53f079138b3f5d9b3c68b04", + "tests/ReleaseArtifactTests/GrpcWeb/packages.lock.json": "fb32043eec1ecd8f2ec860466523722bd3f5ae68a60e38d4d8a62140448f68d8", + "tests/ReleaseArtifactTests/Realtime/RealtimeAotProbe.csproj": "a99e35c36a3e4a19b446d2d2deebff6e60945ed68688c207d23fdfb691c7f2bb", + "tests/ReleaseArtifactTests/Realtime/packages.lock.json": "0dbb97585be8af174a773fe14e3a8a216e54f66a3694ab7c9de3870e50971525" + }, + "frameworkVersions": { + "dotnetSdk": "10.0.400" + }, + "previousReceipt": "eng/policy/dependency-reviews/nat-14-r1.json", + "namingToolCandidate": { + "package": "ArcForges.Contracts.Validation", + "version": "1.0.0-ci.205.1", + "contentHash": "OWvMF30zpN6ms2KlQCtax5P7TACrgK50Vb0drN5zy4iXgk2TZd+5UU+LDGaA2ioowA1EQIZx8ZUyRoayZaHadA==", + "sourceCommit": "696242d16034262ce8b7268e8d157cd0e5bee544", + "licence": "Apache-2.0", + "metadata": "https://api.nuget.org/v3/catalog0/data/2026.09.27.22.53.05/arcforges.contracts.validation.1.0.0-ci.205.1.json", + "publication": "https://github.com/ArcForges/Contracts/actions/runs/36356146302" + } + }, + "nugetClosure": { + "arcforges.application.abstractions/1.0.0-ci.29.1": { + "contentHash": "8ICXm8LvJsaXgNz6RfiHSoxxqxE3jLJMz8IWCcweW0FIzv6isEl3SMH+pbBATNKIqApDmCN78FT/csCAqMBMRA==", + "licence": "AGPL-3.0-only", + "nuspecSha256": "ee357e2c21126b5278bba1b94212302e1dd30e25af1adbd45d185f0863ec634a", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.application.abstractions/1.0.0-ci.29.1/arcforges.application.abstractions.nuspec", + "internalPublisher": { + "repository": "ArcForges/DesktopPlatform", + "workflow": "publish-nuget.yml", + "sourceCommit": "ace60538849047184954baed09fb05ec7676d367", + "feed": "https://api.nuget.org/v3/index.json" + }, + "classification": "Exact existing DesktopPlatform publication consumed only by the local FND.07 acceptance runner; same AGPL boundary." + }, + "arcforges.contracts.events/1.0.0-ci.270.1": { + "contentHash": "kFPTrTBtWkekjE+OT6weJOGqfnJNBCeXtjSnRK6VDFHNkSujKFHJwHRm27xa8lNi0tmDEmjXb5G8shckXgXaUA==", + "licence": "Apache-2.0", + "nuspecSha256": "d8ccfdc8593ab55022acb0d7acb76b6538e8a938ab227604f20b4998ab7f6d8f", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.events/1.0.0-ci.270.1/arcforges.contracts.events.nuspec", + "classification": "Exact CON.11 Events candidate (Contracts source 4047930) providing the generated EventService and ExecutionService clients and the StreamFrame records; used only by the non-packable PRF.06 realtime AOT probe; project-conditioned pin.", + "internalPublisher": { + "repository": "ArcForges/Contracts", + "workflow": "ci.yml", + "sourceCommit": "4047930ef2207e8a4c6de9e7ef679f0d9359a8e4", + "feed": "https://api.nuget.org/v3/index.json" + } + }, + "arcforges.contracts.foundation/1.0.0-ci.113.1": { + "contentHash": "k51yFjKnec92FJEfOM3/6YEhnbi1wfO4gztqjlhLumg7USTjVnA7ab7rE4X/H1cDb62Ny4c2ufn8Xi/CtBaZnA==", + "licence": "Apache-2.0", + "nuspecSha256": "7726a233f9ce81c4373a2ab9423c33d566b919bb2cb90d2b5577cf844bbbbfc4", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.foundation/1.0.0-ci.113.1/arcforges.contracts.foundation.nuspec", + "classification": "Exact published Contracts.Foundation producer and its generated protobuf runtime; Apache-2.0/BSD-3-Clause permitted inside AGPL consumers." + }, + "arcforges.contracts.foundation/1.0.0-ci.216.1": { + "contentHash": "+RGxNiaoOBt+j2IOSpUa2WWD4Bnkk8Ten7jbOVFwdUVF8jUQRCCvAGhrPTzZpnr6Sk5MSoA95+pJYgN8T+jEtg==", + "licence": "Apache-2.0", + "nuspecSha256": "b2704457f3245e4c44200ed4e8dd87054a81cbe53faa31cd384aad92db78839e", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.foundation/1.0.0-ci.216.1/arcforges.contracts.foundation.nuspec", + "classification": "Internal ArcForges Contracts.Foundation dependency already admitted at exact 1.0.0-ci.216.1; consumed by the existing PRF.04 non-packable AOT probe and PLT.19 ArcForges.Capabilities/ArcForges.Capabilities.Tests consumers. Source commit and original candidate publication are recorded in the Plan ledger.", + "internalPublisher": { + "repository": "ArcForges/Contracts", + "workflow": "ci.yml", + "sourceCommit": "eb650e5b9ef7fc06f0eb79ee167b896e4252b50e", + "feed": "https://api.nuget.org/v3/index.json" + } + }, + "arcforges.contracts.foundation/1.0.0-ci.270.1": { + "contentHash": "zYmoUzz2qQcT8IzCmDzVxWSrgxDQFUqf7Z8OkJ0FLPTNM3VC8uSrlN6McQNKt1a1kOHLMOhbWqEE1TPmVR6M5Q==", + "licence": "Apache-2.0", + "nuspecSha256": "818e4e3e34012bf54a430708ba5ab6a002e653740881fea9a679c532b0eeddea", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.foundation/1.0.0-ci.270.1/arcforges.contracts.foundation.nuspec", + "classification": "Exact CON.11 Foundation candidate (Contracts source 4047930, CI run 36809684358), consumed only by the non-packable PRF.06 realtime AOT probe through a project-conditioned version update.", + "internalPublisher": { + "repository": "ArcForges/Contracts", + "workflow": "ci.yml", + "sourceCommit": "4047930ef2207e8a4c6de9e7ef679f0d9359a8e4", + "feed": "https://api.nuget.org/v3/index.json" + } + }, + "arcforges.contracts.localrpc.platform/1.0.0-ci.216.1": { + "contentHash": "TdmfXUx/V6ImX9AijHou/1pzwWfQPOid3pHR2OnYNDPXj7IxylfUmiDzSIrhwaJKXTgxM3UpuAiiNJxnqjZB2A==", + "licence": "Apache-2.0", + "nuspecSha256": "2e9af7a67637cdff08601ce3fb2d20a445fd34c7d535dc4d3508ff10bc9a3c1b", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.localrpc.platform/1.0.0-ci.216.1/arcforges.contracts.localrpc.platform.nuspec", + "classification": "Exact CON.05 generated LocalRpc producer consumed only by the PRF.04 non-packable AOT probe; source commit and original candidate publication are recorded in the Plan ledger.", + "internalPublisher": { + "repository": "ArcForges/Contracts", + "workflow": "ci.yml", + "sourceCommit": "eb650e5b9ef7fc06f0eb79ee167b896e4252b50e", + "feed": "https://api.nuget.org/v3/index.json" + } + }, + "arcforges.contracts.localrpc.sandbox/1.0.0-ci.216.1": { + "contentHash": "u0NGE5MZvolG4Bc2t2nPKiccsP8fKcIPQcnAFCSmMBgL1+q6KwHgr9mlFF1FDyW7Seu9JVINWYOsK38RH5hfkg==", + "licence": "Apache-2.0", + "nuspecSha256": "c65862c9eedf3402636ddc28c67fbd9a54a9a5b6214bf4bb816b4bb2c47805b6", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.localrpc.sandbox/1.0.0-ci.216.1/arcforges.contracts.localrpc.sandbox.nuspec", + "classification": "Exact CON.04 generated ContentSandboxService producer (same Contracts source commit as the admitted LocalRpc.Platform candidate), consumed by the PLT.45 non-packable ContentSandbox contracts facade, broker, helper host, test project and test-only fixture; its nuspec dependencies (Foundation 1.0.0-ci.216.1, Google.Protobuf 3.36.1, Grpc.Core.Api 2.84.0) are already admitted. Source commit and original candidate publication are recorded in the Plan ledger.", + "internalPublisher": { + "repository": "ArcForges/Contracts", + "workflow": "ci.yml", + "sourceCommit": "eb650e5b9ef7fc06f0eb79ee167b896e4252b50e", + "feed": "https://api.nuget.org/v3/index.json" + } + }, + "arcforges.contracts.publicapi/1.0.0-ci.216.1": { + "licence": "Apache-2.0", + "nuspecSha256": "2001724ed917f3754749a16ff5cd458ec50ae6d8a26025456ad21732f4954fad", + "classification": "Exact published Contracts candidate. It is a transitive dependency of ArcForges.Sdk.Contracts 1.0.0-ci.216.1 in the existing Capabilities and Contributions graphs, and the non-packable PRF.05 probe also references it directly for the generated HelloService client at the same version. Apache-2.0 is permitted inside AGPL consumers.", + "contentHash": "cj1wcu/v8bSGL0pGbKYMcJGFFs7SGF3jsG6GXm49cpj3Cal/tl2orj5jdoDBweOH0LeWZisNfFhfXkvnKR9wfQ==", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.publicapi/1.0.0-ci.216.1/arcforges.contracts.publicapi.nuspec", + "internalPublisher": { + "repository": "ArcForges/Contracts", + "workflow": "ci.yml", + "sourceCommit": "eb650e5b9ef7fc06f0eb79ee167b896e4252b50e", + "feed": "https://api.nuget.org/v3/index.json" + } + }, + "arcforges.contracts.publicapi/1.0.0-ci.270.1": { + "contentHash": "0dz653V64rZFFKgDDfEKbH2lqzJ5GVeRMd1dK7ZOr156pEPQMqZL89RQY//WZ9JiSVWSxWemX2JI6JyKKjmibQ==", + "licence": "Apache-2.0", + "nuspecSha256": "f18f43b539a1ad039c3f11e7a5eddeb3463836186b4f28c7eafb50636015fdd6", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.publicapi/1.0.0-ci.270.1/arcforges.contracts.publicapi.nuspec", + "classification": "Exact CON.11 PublicApi candidate (Contracts source 4047930), a direct reference of the non-packable PRF.06 realtime AOT probe for ExecutionOwner and the shared annex 10 records; project-conditioned pin.", + "internalPublisher": { + "repository": "ArcForges/Contracts", + "workflow": "ci.yml", + "sourceCommit": "4047930ef2207e8a4c6de9e7ef679f0d9359a8e4", + "feed": "https://api.nuget.org/v3/index.json" + } + }, + "arcforges.foundation/1.0.0-ci.29.1": { + "contentHash": "P5FRjzq8/DOhNS4bI4PAtjjnkIN2XPufXV0vnfyRFyDN12A/QcV1nteBCu4q/Kxg9R0Enlftut9QxYNtMPzDTg==", + "licence": "AGPL-3.0-only", + "nuspecSha256": "e935bdbe249491c131cdc2c9a07dc55a1b9724c664acd3064bd8393a646bdc59", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.foundation/1.0.0-ci.29.1/arcforges.foundation.nuspec", + "internalPublisher": { + "repository": "ArcForges/DesktopPlatform", + "workflow": "publish-nuget.yml", + "sourceCommit": "ace60538849047184954baed09fb05ec7676d367", + "feed": "https://api.nuget.org/v3/index.json" + }, + "classification": "Exact existing DesktopPlatform publication consumed only by the local FND.07 acceptance runner; same AGPL boundary." + }, + "arcforges.sdk.contracts/1.0.0-ci.216.1": { + "licence": "Apache-2.0", + "nuspecSha256": "c7de5c36751d21de28171308dd99c25ec6a451baaf08a0a824534a73260c8a12", + "classification": "Exact published CON producer required to consume generated Invocation from ArcForges.Sdk.Contracts 1.0.0-ci.216.1; direct consumer ArcForges.Capabilities, transitively locked for the existing Capabilities and Contributions production/test projects. Apache-2.0 is permitted inside AGPL consumers.", + "contentHash": "aN/9J61hb7TTPnOBugNHD4Ic3aBc/3zTIsmseTdaCNO/RiaOTwFnsteg7UKg4pYAfuiL3AkTjjSrxmZ3rNIPMg==", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.sdk.contracts/1.0.0-ci.216.1/arcforges.sdk.contracts.nuspec", + "internalPublisher": { + "repository": "ArcForges/Contracts", + "workflow": "ci.yml", + "sourceCommit": "eb650e5b9ef7fc06f0eb79ee167b896e4252b50e", + "feed": "https://api.nuget.org/v3/index.json" + } + }, + "avalonia.buildservices/11.3.2": { + "contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ==", + "licence": "MIT", + "nuspecSha256": "a8846537f7d074b8f9ed753c03678128c36bd046939ee245fad8c575a7cd3e9b", + "source": "https://api.nuget.org/v3-flatcontainer/avalonia.buildservices/11.3.2/avalonia.buildservices.nuspec", + "classification": "Build-only transitive dependency of the isolated PRF.09 verification probe; not admitted to any shipped product closure or package allowlist." + }, + "avalonia.remote.protocol/12.1.3": { + "contentHash": "HcF69DKyihgg5coLOSp7GTqxQvWAy58LMKauwcHA28uSBJVwu0h5+gTNdJA8Sn/nqJVTRDU98zYsk6i1QSwDVQ==", + "licence": "MIT", + "nuspecSha256": "4c0cd5ec137d2a376bab20e62bb2cbbdc9b71ffcb044acf41adeaf25cf5025b1", + "source": "https://api.nuget.org/v3-flatcontainer/avalonia.remote.protocol/12.1.3/avalonia.remote.protocol.nuspec", + "classification": "Build-only transitive dependency of the isolated PRF.09 verification probe; not admitted to any shipped product closure or package allowlist." + }, + "avalonia/12.1.3": { + "contentHash": "l0VGyGn1OwqFrhmIhIHivvrQP0DtBD4Vn2F1vLYgGA2X2qHg0DMVRAln/6xOTLBUYoTopiVAujN0jtH8MpVSeA==", + "licence": "MIT", + "nuspecSha256": "8fd64a84c229548a71e61390f4a310219f50529af0ee53162ffd39e7874ff4dc", + "source": "https://api.nuget.org/v3-flatcontainer/avalonia/12.1.3/avalonia.nuspec", + "classification": "Build-only verification dependency for the isolated PRF.09 Native AOT probe; not admitted to any shipped product closure or package allowlist." + }, + "coverlet.collector/10.0.1": { + "contentHash": "27jXSV/0DbVqF5jDrAxuQFZ9oaz6gmG03p8ttxAFk+X0M4woFYj7MoWDLCna5EGLb0CE6OE7X6ZH3Wt5smTtaA==", + "licence": "MIT", + "nuspecSha256": "d73992a0c9eb178bf52fde96055930e0ac5b05e53c73ca5c0595f01e61210910", + "source": "https://api.nuget.org/v3-flatcontainer/coverlet.collector/10.0.1/coverlet.collector.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "google.protobuf/3.36.1": { + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==", + "licence": "BSD-3-Clause", + "nuspecSha256": "d6a4a567ce4889e25d098fd39d13a65ce05c847e8574818edc0d1eee6277eb40", + "source": "https://api.nuget.org/v3-flatcontainer/google.protobuf/3.36.1/google.protobuf.nuspec", + "classification": "Exact published Contracts.Foundation producer and its generated protobuf runtime; Apache-2.0/BSD-3-Clause permitted inside AGPL consumers." + }, + "grpc.aspnetcore.server/2.83.0": { + "contentHash": "rWeSaXaSPIIerc82lhHKqmzlp/sgLnG+Zw1c3YEEVp/un+hJKPxJOXWuGUOS2cs3EvcZBZ2tliXsnAg5Mdk2Ew==", + "licence": "Apache-2.0", + "nuspecSha256": "b9a51de789abe619de7ee4bb9ec9f30452c4bca13d514e53ac574a0bb96116e3", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.aspnetcore.server/2.83.0/grpc.aspnetcore.server.nuspec", + "classification": "Exact permitted gRPC ASP.NET Core server candidate; used by the non-packable PRF.04 AOT proof only." + }, + "grpc.core.api/2.84.0": { + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==", + "licence": "Apache-2.0", + "nuspecSha256": "bba00f5d5be8423dd351662e313648b0abd13100ef67d55f6fc29de44c544a96", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.core.api/2.84.0/grpc.core.api.nuspec", + "classification": "Exact CON.05 transitive and permitted gRPC client dependency; consumed through ArcForges.Sdk.Contracts 1.0.0-ci.216.1 by the PLT.24 Capabilities/Contributions lock graphs and used by the non-packable PRF.04 AOT proof only as a build-time candidate." + }, + "grpc.net.client.web/2.84.0": { + "contentHash": "6KB6IFGx+KfGMaN06nATBVgvgsOEVf12/nJHvdAPsY2mULmWCSn4moFUT8UnCnQ19C3tpGbc2CnB1/e0hVUmaA==", + "licence": "Apache-2.0", + "nuspecSha256": "da47ce027ddbbbf7c0b1f242b1efe64a5aeaa2959da54716a100fcc6e4520efb", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client.web/2.84.0/grpc.net.client.web.nuspec", + "classification": "Exact direct gRPC-Web client handler shared by PRF.05 and PRF.06; the net10.0 dependency group is empty, so it adds no transitive coordinate. Used only by the non-packable PRF.05 Native AOT probe." + }, + "grpc.net.client/2.84.0": { + "contentHash": "My6Fms2YCyAbihTBZZ3kLS39iTY2pkMAzGDBTC6XhPOujg70eB07AEJ7tPiW3tZ70bhFzHcc3+d4j3DaIPF3aA==", + "licence": "Apache-2.0", + "nuspecSha256": "32a43f3448ab90bb71c946533db4d5135cef4728a3ee29bf88d9a0cbf1e67413", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client/2.84.0/grpc.net.client.nuspec", + "classification": "Exact permitted gRPC .NET client candidate; used by the non-packable PRF.04 AOT proof only." + }, + "grpc.net.common/2.84.0": { + "contentHash": "DPxm/H8G/lDRrz997wM0M2zN/PGqVeZKDakhBbtABEbv6WIW4HI9g3BvntBQT6ZUROuGacMzfyyF9e5kiTVFfQ==", + "licence": "Apache-2.0", + "nuspecSha256": "f621aa566e63340b572a4f0bd2b996b934407c77b8cdf121f38578e93ee796cb", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.common/2.84.0/grpc.net.common.nuspec", + "classification": "Exact shared gRPC transport dependency selected by the permitted server/client candidates; used by the non-packable PRF.04 AOT proof only." + }, + "microcom.runtime/0.11.6": { + "contentHash": "NdNWGDiZ6eS/Mf/9+QHR91cj1K7Hy+PX9yrHI/zM7xFYuj9IWT2uxtB6sCHjrnxAeLV9fut1R6zHDUGKX6f9lQ==", + "licence": "MIT", + "nuspecSha256": "c7aeab4b73acdba3bb8e66865c2bbe67566b8593d972be2089181344adef345f", + "source": "https://api.nuget.org/v3-flatcontainer/microcom.runtime/0.11.6/microcom.runtime.nuspec", + "classification": "Build-only transitive dependency of the isolated PRF.09 verification probe; not admitted to any shipped product closure or package allowlist." + }, + "microsoft.applicationinsights/2.23.0": { + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "licence": "MIT", + "nuspecSha256": "184c649613d487d2b11466882ad83e375bf94f6aa228afae78dd4005bb0bb2a0", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights/2.23.0/microsoft.applicationinsights.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "microsoft.bcl.asyncinterfaces/6.0.0": { + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==", + "licence": "MIT", + "nuspecSha256": "3a965c424d56f1cee169d88cc9c04f2c04f01bd65f20147b26ac093d761beec9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.bcl.asyncinterfaces/6.0.0/microsoft.bcl.asyncinterfaces.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "microsoft.codeanalysis.netanalyzers/10.0.401": { + "contentHash": "qGPLk4hknvVp6j9Lowa0qykw1cK7CyEyyy2aVs4dkOGroOMHJLRsMpUT53IKvYT6q78jxv96FZhzy3BrKCPgmA==", + "licence": "MIT", + "nuspecSha256": "a8e2d43ca608c2f6e4039020149653abd188821fd8db1bed4fceb60fde2f21e1", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.codeanalysis.netanalyzers/10.0.401/microsoft.codeanalysis.netanalyzers.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "microsoft.codecoverage/18.10.1": { + "contentHash": "tV7tCyp/t+DuwQugLkCeUN7NBFFB4fEGRPr0gkMQ2nQgAvQQjmGlZZi1dsLzbyoe/A0mbnKybmkIh8qhT9p4Zw==", + "licence": "MIT", + "nuspecSha256": "e7473877d0450d94c304469a0e89932731f4ba498edd3583211a0cda6ca68c57", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.codecoverage/18.10.1/microsoft.codecoverage.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "microsoft.data.sqlite.core/10.0.12": { + "contentHash": "2aL9eL5HQr0V4b8V909SD+jQAx6qnTTl7sT/v3D8dIvmr2UGdpZIQxqm1pj2Js3e4EYTvpw0oMH7DjapJJuPPw==", + "licence": "MIT", + "nuspecSha256": "56030907327ba858664e4c2cbc6547ad56d766d900bc323c14408fcd42fc46a0", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.data.sqlite.core/10.0.12/microsoft.data.sqlite.core.nuspec", + "classification": "runtime dependency of ArcForges.Persistence.Sqlite; exact package retained as NuGet dependency, not copied source" + }, + "microsoft.data.sqlite/10.0.12": { + "contentHash": "xUq9xUc653ue7ac2rMfCaf8UQN+U3zS8poWYlugycpvJllplMM0Mk7v/+MM0CTlu9zGvbGML4BPH8YWhrUOfGw==", + "licence": "MIT", + "nuspecSha256": "8e1c47c77197daaac1584979b7bb42a9a8f6deaff9f6d18029b3a689a2107c6c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.data.sqlite/10.0.12/microsoft.data.sqlite.nuspec", + "classification": "runtime dependency of ArcForges.Persistence.Sqlite; exact package retained as NuGet dependency, not copied source" + }, + "microsoft.dotnet.ilcompiler/10.0.11": { + "contentHash": "yloFUsIQOzspIsn2cDHrbgikKaWHAZspzj0EWxnWJJpFC89IBW64OXgCECsrrnzGWQvy1iLX9r5/R4zBf7Am3g==", + "licence": "MIT", + "nuspecSha256": "2c16a1f000bd7a7e5c0864d2b097d5d00afc5d723770751bc6228bd463c6cc56", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.dotnet.ilcompiler/10.0.11/microsoft.dotnet.ilcompiler.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "microsoft.extensions.dependencyinjection.abstractions/8.0.1": { + "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==", + "licence": "MIT", + "nuspecSha256": "1ed9fd0c77d67cc75208b3268df565762916a992f3e246973430dbd77ede2413", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection.abstractions/8.0.1/microsoft.extensions.dependencyinjection.abstractions.nuspec", + "classification": "Transitive test-host dependency of the ArcForges.LocalRpc.Tests xUnit/Kestrel closure; consumed only by the non-packable offline test project." + }, + "microsoft.extensions.logging.abstractions/8.0.1": { + "contentHash": "RIFgaqoaINxkM2KTOw72dmilDmTrYA0ns2KW4lDz4gZ2+o6IQ894CzmdL3StM2oh7QQq44nCWiqKqc4qUI9Jmg==", + "licence": "MIT", + "nuspecSha256": "7593932df3e935a8e6670efad230c82967bc45ea4065186d9b886670408b95cc", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging.abstractions/8.0.1/microsoft.extensions.logging.abstractions.nuspec", + "classification": "Transitive test-host dependency of the ArcForges.LocalRpc.Tests xUnit/Kestrel closure; consumed only by the non-packable offline test project." + }, + "microsoft.net.illink.tasks/10.0.11": { + "contentHash": "IBf7lbovvjGWVWXZX5cJ/cO0WXbId0Zq4BuSeT94mGZuOAP66oMeH9PTBZ9Jpp3Jb6jtK0qm/NyUbPRo1gC/wQ==", + "licence": "MIT", + "nuspecSha256": "6c10c93cd2958d71fc46d60c940044071e15b57a75d469d68adf80f0dff57da1", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.illink.tasks/10.0.11/microsoft.net.illink.tasks.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "microsoft.net.test.sdk/18.10.1": { + "contentHash": "SxCFvJE/2ltUQgCgv6+Ixagohw4sZc9hM7Wid2foaNM7paBscktoHLoRDz91fW8wbHG87Rg9Ku59/8PXumqUrA==", + "licence": "MIT", + "nuspecSha256": "84c823b74b806f5b1f4cce2bdaf81ba78abbe0ae93c9828f578205f3d48abca8", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.test.sdk/18.10.1/microsoft.net.test.sdk.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "microsoft.testing.extensions.telemetry/2.4.0": { + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "licence": "MIT", + "nuspecSha256": "5a13a0f3b3da2e1ea48c7faac4f0d3a8ca570a562ec4928e8e59d8f10efa9738", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.telemetry/2.4.0/microsoft.testing.extensions.telemetry.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "microsoft.testing.extensions.trxreport.abstractions/2.4.0": { + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "licence": "MIT", + "nuspecSha256": "f99165b201b607209e5f03db0b2be881d020d45da108028c011fa2b2f6b57b99", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.trxreport.abstractions/2.4.0/microsoft.testing.extensions.trxreport.abstractions.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "microsoft.testing.platform.msbuild/2.4.0": { + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "licence": "MIT", + "nuspecSha256": "1fe1b7f9981ae944b65a9cf851ca791bd5e0dd66dedd5d114f838aeecf5bea4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform.msbuild/2.4.0/microsoft.testing.platform.msbuild.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "microsoft.testing.platform/2.4.0": { + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==", + "licence": "MIT", + "nuspecSha256": "66933f17979c2b024e80d6c8b4144150ecc5773153ee6a485aa430645da96b5d", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform/2.4.0/microsoft.testing.platform.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "microsoft.testplatform.objectmodel/18.10.1": { + "contentHash": "o+PuLDRVp1iwiVAPBADZItXgm9Ck23XtPhE4kZtZVi0uYnMiiz1Dx9Cjh75kJ+ijfqnORL3VOTOaQVKi0f/ixg==", + "licence": "MIT", + "nuspecSha256": "3af65b097b8d4c5bc7c1242e3f56fdb3578a38b313f3bdd7472257f1cdd8d98b", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testplatform.objectmodel/18.10.1/microsoft.testplatform.objectmodel.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "microsoft.testplatform.testhost/18.10.1": { + "contentHash": "KYz2omqkoz9E2mWa4fdgT+Ycie1wi5968EmyJlusjlp87RxxtA2FvDm8tcBEYIDzLnkWGnsKjE4d13Z7/rxkQA==", + "licence": "MIT", + "nuspecSha256": "801559164f0ab5079bad2f8bfa206046919f6a97ae2af8ef77d7a0bc80317457", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testplatform.testhost/18.10.1/microsoft.testplatform.testhost.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "microsoft.win32.registry/5.0.0": { + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "licence": "MIT", + "nuspecSha256": "eb8c4b9d4ebe62d8528dec6ab2a752c2e3488f28f882a6c45a33dcc21eacc58e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "runtime.linux-x64.microsoft.dotnet.ilcompiler/10.0.11": { + "contentHash": "0lJviBev2CvBdrLhMCJ1hmwOKgjbk1ccRy+brEewywtK4TMft1WaVXv112j6OuTQg8ZMbnN/i3KRD3S6o4NJDw==", + "licence": "MIT", + "nuspecSha256": "0d304c7cb54460b35cc188ca792aeb0f81772a6ac85d891e23d98889101aa3a0", + "source": "https://api.nuget.org/v3-flatcontainer/runtime.linux-x64.microsoft.dotnet.ilcompiler/10.0.11/runtime.linux-x64.microsoft.dotnet.ilcompiler.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "runtime.osx-arm64.microsoft.dotnet.ilcompiler/10.0.11": { + "contentHash": "4cxybmyqJI3zZIKkNM1Ft0aq6Q4q6b4SJ11Np5j9lrHHurfheI66dUshpZD/UEMjMqAdg9O03AABAYLYSUzwyw==", + "licence": "MIT", + "nuspecSha256": "3c8c812ae7efd5e2f30b0de43fcd131582f26d1f4a72736f9922688286d44feb", + "source": "https://api.nuget.org/v3-flatcontainer/runtime.osx-arm64.microsoft.dotnet.ilcompiler/10.0.11/runtime.osx-arm64.microsoft.dotnet.ilcompiler.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "runtime.osx-x64.microsoft.dotnet.ilcompiler/10.0.11": { + "contentHash": "Nf7MRJg0HZ4G7icNbW8T49O/9UU6x8XoUPffkk3Uh+r+mDAUFHkhIptUc0wDdIRrouJrLt0IJp0LiaQJUB7DbA==", + "licence": "MIT", + "nuspecSha256": "b350157b66441409caea6b98bba20e59277ac91c18af0772223781a18b2cdad4", + "source": "https://api.nuget.org/v3-flatcontainer/runtime.osx-x64.microsoft.dotnet.ilcompiler/10.0.11/runtime.osx-x64.microsoft.dotnet.ilcompiler.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "runtime.win-arm64.microsoft.dotnet.ilcompiler/10.0.11": { + "contentHash": "vy/AJM7NafhIa+4EsJrh//5Ba3HW3purZh+KR0Bpt/R20w3s06ZrUsdwHywUs+5E7PfEtbXBcLlebKFusSLBkQ==", + "licence": "MIT", + "nuspecSha256": "3f546e9d7d6ba67a8a82544e042b34b02df722122ad56b0a7aa6c33cbd0fc006", + "source": "https://api.nuget.org/v3-flatcontainer/runtime.win-arm64.microsoft.dotnet.ilcompiler/10.0.11/runtime.win-arm64.microsoft.dotnet.ilcompiler.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "runtime.win-x64.microsoft.dotnet.ilcompiler/10.0.11": { + "contentHash": "D6ZKMo3NeTnos7l5S8bMmiPm4LK+y15Q/uduyPw7FX+xKbeWVWdwlcZGkLLPjLvW4ZO6o3ACanq5HsSXJgrt0g==", + "licence": "MIT", + "nuspecSha256": "e65322f78f21c985eea3b00e9f7cbc413d7b26a997ae861b08ee5265fc49b412", + "source": "https://api.nuget.org/v3-flatcontainer/runtime.win-x64.microsoft.dotnet.ilcompiler/10.0.11/runtime.win-x64.microsoft.dotnet.ilcompiler.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "sqlitepclraw.bundle_e_sqlite3/2.1.12": { + "contentHash": "mAgscpQMLw5/nfA1Q5oJVAT29yROUo1ifZGbbTpx/lwZpSxMUGoYbKfmvdm8oXER+RzxqBmmQzeBEVKfeHv2nw==", + "licence": "Apache-2.0", + "nuspecSha256": "2faa9f418c3b8831efdf92df6aca8322ffa9f7a556e915b46e5daa8e1f8e3233", + "source": "https://api.nuget.org/v3-flatcontainer/sqlitepclraw.bundle_e_sqlite3/2.1.12/sqlitepclraw.bundle_e_sqlite3.nuspec", + "classification": "runtime dependency of ArcForges.Persistence.Sqlite; exact package retained as NuGet dependency, not copied source" + }, + "sqlitepclraw.core/2.1.12": { + "contentHash": "ETpNw9DY3ckWLgRRAeCHj+GKOuPi61aeczkXhgHexUvqoZBAYg8RYESE2J7O1M7+o6QbdSEZwrw9bfqztUVWXg==", + "licence": "Apache-2.0", + "nuspecSha256": "8ab9189071aef856b12a9a3d68ac055a01ec6f8fc51b85ac1029e7c5ba12eecc", + "source": "https://api.nuget.org/v3-flatcontainer/sqlitepclraw.core/2.1.12/sqlitepclraw.core.nuspec", + "classification": "runtime dependency of ArcForges.Persistence.Sqlite; exact package retained as NuGet dependency, not copied source" + }, + "sqlitepclraw.lib.e_sqlite3/2.1.12": { + "contentHash": "fWi8Dbknuhgg72fWinIdjXVaqO1hHL4YBBwVLnr7e1c9TAZwJ0QE38j9syW1hwx6HaqEVTwI+O07WPdZn8Rp0w==", + "licence": "Apache-2.0", + "nuspecSha256": "0e82e5cda14ccb8c546596d39393a6a9ba94146ffeeac86069df29ce9d57a731", + "source": "https://api.nuget.org/v3-flatcontainer/sqlitepclraw.lib.e_sqlite3/2.1.12/sqlitepclraw.lib.e_sqlite3.nuspec", + "classification": "runtime dependency of ArcForges.Persistence.Sqlite; exact package retained as NuGet dependency, not copied source" + }, + "sqlitepclraw.provider.e_sqlite3/2.1.12": { + "contentHash": "W3oH4XIfCzFrgUSDKHhN6N+dgzA5YHOR2VxX8GB6Qy7CyrJJgxPEG8NirgYWlPQC5P2jz2knSsexWu4tDUL33g==", + "licence": "Apache-2.0", + "nuspecSha256": "7e5b604b8ad8121fa75f01eec61f56989b717fadfd530188a57d35759d93f573", + "source": "https://api.nuget.org/v3-flatcontainer/sqlitepclraw.provider.e_sqlite3/2.1.12/sqlitepclraw.provider.e_sqlite3.nuspec", + "classification": "runtime dependency of ArcForges.Persistence.Sqlite; exact package retained as NuGet dependency, not copied source" + }, + "system.security.accesscontrol/6.0.1": { + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", + "licence": "MIT", + "nuspecSha256": "2fb3325fbcea2d5eee1eb5666fd391fac8498f7acefe34f922aaceafea5e9df3", + "source": "https://api.nuget.org/v3-flatcontainer/system.security.accesscontrol/6.0.1/system.security.accesscontrol.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "xunit.analyzers/2.1.0": { + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==", + "licence": "Apache-2.0", + "nuspecSha256": "8d8fb2ff8c8a1505020e13d190a093af179b994eebce42ce03457ed9497e7b68", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.analyzers/2.1.0/xunit.analyzers.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "xunit.runner.visualstudio/3.1.5": { + "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==", + "licence": "Apache-2.0", + "nuspecSha256": "0e34c46e1420cea8e5260772ff19bcb3d3faa7709136d2d3ba157f48a97362ba", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.runner.visualstudio/3.1.5/xunit.runner.visualstudio.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "xunit.v3.assert/4.0.1": { + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==", + "licence": "Apache-2.0", + "nuspecSha256": "3f2aef448f6c8e12cb7fa5d6ba111ea5311f530862a3e5fd9a63f153bec1f3af", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.assert/4.0.1/xunit.v3.assert.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "xunit.v3.common/4.0.1": { + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "licence": "Apache-2.0", + "nuspecSha256": "bdf4e653bae8f75b5392928a37bf108fcad4d2a5308524420828e82f3e79ded5", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.common/4.0.1/xunit.v3.common.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "xunit.v3.core.mtp-v2/4.0.1": { + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "licence": "Apache-2.0", + "nuspecSha256": "51c8cc20af9bb6071e1df8454579c9048597f5c169db3470dc5057be9844f1da", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.core.mtp-v2/4.0.1/xunit.v3.core.mtp-v2.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "xunit.v3.extensibility.core/4.0.1": { + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "licence": "Apache-2.0", + "nuspecSha256": "0def7df8a35e99e328a1b8799e169592b961ee06a3f7aac556a42ee8464cecf8", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.extensibility.core/4.0.1/xunit.v3.extensibility.core.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "xunit.v3.mtp-v2/4.0.1": { + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "licence": "Apache-2.0", + "nuspecSha256": "9134992ab09b0b8545f7fa31f6ab3c84a659b58493c7afca586a4977ee8998e2", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.mtp-v2/4.0.1/xunit.v3.mtp-v2.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "xunit.v3.runner.common/4.0.1": { + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "licence": "Apache-2.0", + "nuspecSha256": "2c80dbdeee78193f042e5b5e32c06d3b1d828659fb156c132f82574a5330799f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.common/4.0.1/xunit.v3.runner.common.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "xunit.v3.runner.inproc.console/4.0.1": { + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "licence": "Apache-2.0", + "nuspecSha256": "621fc57c6e5ab43360c5933415c98727f36abf658d1a7d9793159a2b827d4a5f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.inproc.console/4.0.1/xunit.v3.runner.inproc.console.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + }, + "xunit.v3/4.0.1": { + "contentHash": "KpDYf6jFjYfOchuRJOAXPOpwN03hLE1AF4kWzCUSnhr8JZMiGH4BT1YidCcm6BE1H4E90GyJzuKoCwXbg9PXmA==", + "licence": "Apache-2.0", + "nuspecSha256": "c0407f2bfbba8b98dfc27d55a8707c47afffa3debf66b1b562aba818159f43e8", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3/4.0.1/xunit.v3.nuspec", + "classification": "build-test-tooling; runtime package closure remains packaging allowlist owned" + } + }, + "pythonClosure": { + "pre-commit/4.6.0": { + "hashes": [ + "718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", + "e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b" + ], + "licence": "MIT", + "evidence": [ + { + "source": "pre_commit-4.6.0.dist-info/LICENSE", + "sha256": "ea2ca27cba7cc35822d95a46d59bcd3cc88e196592e6390d1949a359ffc990e8" + }, + { + "source": "pre_commit-4.6.0.dist-info/METADATA", + "sha256": "4db1e1d0aa58955d863cb9e0382470ff2b73a2baad9110c14094c70e9f29dd42" + } + ], + "scope": "CI hook tooling only; hash-required requirements restore, not shipped in capability packages" + }, + "cfgv/3.5.0": { + "hashes": [ + "a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", + "d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132" + ], + "licence": "MIT", + "evidence": [ + { + "source": "cfgv-3.5.0.dist-info/LICENSE", + "sha256": "01fc3f8031a672b3f5d7d8ac262e432f3ea812809f5697d6bc5b270bf6446561" + }, + { + "source": "cfgv-3.5.0.dist-info/METADATA", + "sha256": "ae126b80e15f39d0feff7368cefea2ea6478fdcedcde52918d0f58b668e19137" + } + ], + "scope": "CI hook tooling only; hash-required requirements restore, not shipped in capability packages" + }, + "identify/2.6.19": { + "hashes": [ + "20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", + "6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842" + ], + "licence": "MIT", + "evidence": [ + { + "source": "identify-2.6.19.dist-info/LICENSE", + "sha256": "edbc2ad3b7084fac873e1b1b450fd370d914eb97f7f31263b4ff65f47dc10c43" + }, + { + "source": "identify-2.6.19.dist-info/METADATA", + "sha256": "6bd3f4b6084b3aa9bbfdd4631b2edffdc2908b396dfc2971891d0356bccb4965" + } + ], + "scope": "CI hook tooling only; hash-required requirements restore, not shipped in capability packages" + }, + "nodeenv/1.10.0": { + "hashes": [ + "5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", + "996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb" + ], + "licence": "BSD-3-Clause", + "evidence": [ + { + "source": "nodeenv-1.10.0.dist-info/METADATA", + "sha256": "a8424896a12fd1be10526834176fc754cd296c900f40da655fb2e03c1b7eb921" + }, + { + "source": "nodeenv-1.10.0.dist-info/licenses/LICENSE", + "sha256": "606faf42d48b54d539dae99db6fecd48544535587e27275b319a3f36113cb6fa" + } + ], + "scope": "CI hook tooling only; hash-required requirements restore, not shipped in capability packages" + }, + "pyyaml/6.0.3": { + "hashes": [ + "00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", + "0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", + "02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", + "02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", + "0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", + "10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", + "16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", + "1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", + "1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", + "214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", + "2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", + "22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", + "27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", + "28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", + "2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", + "34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", + "37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", + "3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", + "3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", + "41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", + "418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", + "44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", + "4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", + "4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", + "501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", + "5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", + "5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", + "5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", + "5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", + "5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", + "5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", + "5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", + "6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", + "64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", + "652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", + "66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", + "66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", + "6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", + "79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", + "7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", + "7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", + "8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", + "850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", + "8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", + "8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", + "8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", + "9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", + "93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", + "96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", + "9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", + "9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", + "9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", + "a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", + "a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", + "b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", + "b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", + "b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", + "b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", + "ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", + "bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", + "c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", + "c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", + "c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", + "c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", + "d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", + "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", + "ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", + "eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", + "ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", + "efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", + "f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", + "fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", + "fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" + ], + "licence": "MIT", + "evidence": [ + { + "source": "pyyaml-6.0.3.dist-info/METADATA", + "sha256": "847a701020334386852b6e07bad1053cb0ce3da9ee1846e7e9d02b24e8bb3f8d" + }, + { + "source": "pyyaml-6.0.3.dist-info/licenses/LICENSE", + "sha256": "8d3928f9dc4490fd635707cb88eb26bd764102a7282954307d3e5167a577e8a4" + } + ], + "scope": "CI hook tooling only; hash-required requirements restore, not shipped in capability packages" + }, + "virtualenv/21.5.1": { + "hashes": [ + "55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", + "dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8" + ], + "licence": "MIT", + "evidence": [ + { + "source": "virtualenv-21.5.1.dist-info/METADATA", + "sha256": "a2b4e6261451dbe7dfc3ff6b720292889b9b02edb9023c4f3f93337bfa2f3187" + }, + { + "source": "virtualenv-21.5.1.dist-info/licenses/LICENSE", + "sha256": "5c15919378c5b2aaab7b19cea70d8cdc75f76879e32454e4c0399f8b71d171e9" + } + ], + "scope": "CI hook tooling only; hash-required requirements restore, not shipped in capability packages" + }, + "distlib/0.4.3": { + "hashes": [ + "4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", + "f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed" + ], + "licence": "PSF-2.0", + "evidence": [ + { + "source": "distlib-0.4.3.dist-info/METADATA", + "sha256": "6c988f8c23faa0ef8734f6863b32976d65f5851333161e8a8df3d80fc57e080d" + }, + { + "source": "distlib-0.4.3.dist-info/licenses/LICENSE.txt", + "sha256": "808e10c8a6ab8deb149ff9b3fb19f447a808094606d712a9ca57fead3552599d" + } + ], + "scope": "CI hook tooling only; hash-required requirements restore, not shipped in capability packages" + }, + "filelock/3.29.5": { + "hashes": [ + "6e6034c57a00a020e767f2614a5539863f056de7e7991d6d1473aef7ff73f156", + "8af830889ba3a0ffcefbd6c7d2af8a54012058103771f2e10848222f476a1693" + ], + "licence": "MIT", + "evidence": [ + { + "source": "filelock-3.29.5.dist-info/METADATA", + "sha256": "0221323a2a7129ed6f8672eecd33c5b0e4b78ff2f179ba751ed89b3de8538fec" + }, + { + "source": "filelock-3.29.5.dist-info/licenses/LICENSE", + "sha256": "608c89d5060ae9921adccf3236695bc654a9946e12323ef6c021dfa04e294d48" + } + ], + "scope": "CI hook tooling only; hash-required requirements restore, not shipped in capability packages" + }, + "platformdirs/4.10.0": { + "hashes": [ + "31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", + "fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a" + ], + "licence": "MIT", + "evidence": [ + { + "source": "platformdirs-4.10.0.dist-info/METADATA", + "sha256": "c4ee18f5b6d2b38512a30c289200b1463f298a8261c8fa63f2584ea97e93cf78" + }, + { + "source": "platformdirs-4.10.0.dist-info/licenses/LICENSE", + "sha256": "29e0fd62e929850e86eb28c3fdccf0cefdf4fa94879011cffb3d0d4bed6d4db6" + } + ], + "scope": "CI hook tooling only; hash-required requirements restore, not shipped in capability packages" + }, + "python-discovery/1.4.3": { + "hashes": [ + "ad57d7045a862460d4a235986c33f13ed707d3aeb9153fa47eb7dfd0d4673289", + "b6e1e4a7d9e3f6948c39746ffe8218225162d738ba39d05ab1d2f6c1cac4878c" + ], + "licence": "MIT", + "evidence": [ + { + "source": "python_discovery-1.4.3.dist-info/METADATA", + "sha256": "ffd0a6dec984955e2582e78b7183037478c5ce8ebab633b5b6a1fe2ba2b0906d" + }, + { + "source": "python_discovery-1.4.3.dist-info/licenses/LICENSE", + "sha256": "90e25e1faf2a4aaea8d95ee8e7f5738b052a1acc0c9c58068101f931a86bd588" + } + ], + "scope": "CI hook tooling only; hash-required requirements restore, not shipped in capability packages" + } + } +} diff --git a/eng/policy/licence-boundary.json b/eng/policy/licence-boundary.json index c544cae1..5d8d4b44 100644 --- a/eng/policy/licence-boundary.json +++ b/eng/policy/licence-boundary.json @@ -28,10 +28,6 @@ "path": "native/arcimage-abi/CMakeLists.txt", "kind": "cmake" }, - { - "path": "native/arcpdf-abi/CMakeLists.txt", - "kind": "cmake" - }, { "path": "native/arcimage-abi/windows/arcslate_image_abi.vcxproj", "kind": "msbuild" @@ -176,10 +172,6 @@ "path": "src/Native/ArcForges.Native.Image/ArcForges.Native.Image.csproj", "kind": "msbuild" }, - { - "path": "src/Native/ArcForges.Native.Pdf/ArcForges.Native.Pdf.csproj", - "kind": "msbuild" - }, { "path": "tests/ArchitectureTests/ArcForges.Tests.ArchitectureTests.csproj", "kind": "msbuild" diff --git a/eng/policy/reconciliation/active-projects.json b/eng/policy/reconciliation/active-projects.json index 0e044794..ed3db00d 100644 --- a/eng/policy/reconciliation/active-projects.json +++ b/eng/policy/reconciliation/active-projects.json @@ -20,16 +20,12 @@ }, { "path": "native/CMakeLists.txt", - "blob": "9db191dd8e3ed8863e30ee847b180e8985cb3e0d" + "blob": "76b9e2fe6b1292ff48d16c56661704647dbd273f" }, { "path": "native/arcimage-abi/CMakeLists.txt", "blob": "2af2a384ad1d1dc509a4c86921983ca6ed067931" }, - { - "path": "native/arcpdf-abi/CMakeLists.txt", - "blob": "3e0b207ec7a0440328c3b3e35eb5e9ca1ea5d401" - }, { "path": "native/arcimage-abi/windows/arcslate_image_abi.vcxproj", "blob": "0bb08be2c7631e3771239e36a71a9ea31dc19f39" @@ -152,7 +148,7 @@ }, { "path": "src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj", - "blob": "daf4e45e7e188048e44acfef682015b48a812109" + "blob": "5f6d1f4c61cd0ae2d86afc63941f885c7f831cee" }, { "path": "src/DesktopHelpers/ArcForges.ContentSandbox.Contracts/ArcForges.ContentSandbox.Contracts.csproj", @@ -182,10 +178,6 @@ "path": "src/Native/ArcForges.Native.Image/ArcForges.Native.Image.csproj", "blob": "9132f77575e4d1940f4648f7f208d86cf6087bca" }, - { - "path": "src/Native/ArcForges.Native.Pdf/ArcForges.Native.Pdf.csproj", - "blob": "155342fc571527dc20c1615abd6630dd44d01623" - }, { "path": "tests/ArchitectureTests/ArcForges.Tests.ArchitectureTests.csproj", "blob": "25bfccb216e0a236ff33f076d1e5370b5f4964a2" diff --git a/eng/policy/reconciliation/directories.json b/eng/policy/reconciliation/directories.json index 0b5507f9..904c2d7d 100644 --- a/eng/policy/reconciliation/directories.json +++ b/eng/policy/reconciliation/directories.json @@ -2231,62 +2231,6 @@ "producer": "WP13", "reason": "Required owned directory; implement substantive behavior only in its scheduled producer." }, - { - "owner": "DesktopPlatform", - "path": "src/Native/ArcForges.Native.Pdf", - "disposition": "Keep", - "present": false, - "producer": "WP13", - "reason": "Required owned directory; implement substantive behavior only in its scheduled producer." - }, - { - "owner": "DesktopPlatform", - "path": "src/Native/ArcForges.Native.Pdf.Runtime.linux-arm64", - "disposition": "Keep", - "present": false, - "producer": "WP13", - "reason": "Required owned directory; implement substantive behavior only in its scheduled producer." - }, - { - "owner": "DesktopPlatform", - "path": "src/Native/ArcForges.Native.Pdf.Runtime.linux-x64", - "disposition": "Keep", - "present": false, - "producer": "WP13", - "reason": "Required owned directory; implement substantive behavior only in its scheduled producer." - }, - { - "owner": "DesktopPlatform", - "path": "src/Native/ArcForges.Native.Pdf.Runtime.osx-arm64", - "disposition": "Keep", - "present": false, - "producer": "WP13", - "reason": "Required owned directory; implement substantive behavior only in its scheduled producer." - }, - { - "owner": "DesktopPlatform", - "path": "src/Native/ArcForges.Native.Pdf.Runtime.osx-x64", - "disposition": "Keep", - "present": false, - "producer": "WP13", - "reason": "Required owned directory; implement substantive behavior only in its scheduled producer." - }, - { - "owner": "DesktopPlatform", - "path": "src/Native/ArcForges.Native.Pdf.Runtime.win-arm64", - "disposition": "Keep", - "present": false, - "producer": "WP13", - "reason": "Required owned directory; implement substantive behavior only in its scheduled producer." - }, - { - "owner": "DesktopPlatform", - "path": "src/Native/ArcForges.Native.Pdf.Runtime.win-x64", - "disposition": "Keep", - "present": false, - "producer": "WP13", - "reason": "Required owned directory; implement substantive behavior only in its scheduled producer." - }, { "owner": "DesktopPlatform", "path": "src/SDK", diff --git a/eng/policy/reconciliation/source.json b/eng/policy/reconciliation/source.json index b8b053ca..6846b563 100644 --- a/eng/policy/reconciliation/source.json +++ b/eng/policy/reconciliation/source.json @@ -14,7 +14,7 @@ "owners": 7, "projects": 67, "historical": 166, - "directories": 328, + "directories": 321, "native": 7 } } diff --git a/eng/policy/runtime-ownership.json b/eng/policy/runtime-ownership.json index 656d08fb..08f8555d 100644 --- a/eng/policy/runtime-ownership.json +++ b/eng/policy/runtime-ownership.json @@ -40,10 +40,6 @@ "path": "native/arcimage-abi/CMakeLists.txt", "role": "native-build" }, - { - "path": "native/arcpdf-abi/CMakeLists.txt", - "role": "native-build" - }, { "path": "native/arcimage-abi/windows/arcslate_image_abi.vcxproj", "role": "native-build" @@ -192,10 +188,6 @@ "path": "src/Native/ArcForges.Native.Image/ArcForges.Native.Image.csproj", "role": "aot-library" }, - { - "path": "src/Native/ArcForges.Native.Pdf/ArcForges.Native.Pdf.csproj", - "role": "aot-library" - }, { "path": "tests/ArchitectureTests/ArcForges.Tests.ArchitectureTests.csproj", "role": "test-or-build-tool" diff --git a/eng/provenance/files.json b/eng/provenance/files.json index f09450be..4f35881a 100644 --- a/eng/provenance/files.json +++ b/eng/provenance/files.json @@ -938,7 +938,6 @@ "src/DesktopHelpers/ArcForges.ContentSandbox/Host/HelperResources.cs", "src/DesktopHelpers/ArcForges.ContentSandbox/Host/LinuxSandboxProfile.cs", "src/DesktopHelpers/ArcForges.ContentSandbox/Host/Parsers.cs", - "src/DesktopHelpers/ArcForges.ContentSandbox/Host/PdfTextPager.cs", "src/DesktopHelpers/ArcForges.ContentSandbox/Host/ProfileEnforcement.cs", "src/DesktopHelpers/ArcForges.ContentSandbox/Native/HelperWindowsNative.cs", "src/DesktopHelpers/ArcForges.ContentSandbox/Native/LinuxEnforcement.cs", @@ -952,7 +951,6 @@ "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/LinuxProfileTests.cs", "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/MacProfileTests.cs", "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/OsIsolationTests.cs", - "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/PdfTextPagerTests.cs", "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/packages.lock.json", "src/DesktopHelpers/ArcForges.ContentSandbox/macos/ArcForges.ContentSandbox.entitlements", "src/BuildingBlocks/ArcForges.Security/CapabilityEnforcement/ArcForges.Security.CapabilityEnforcement.csproj", @@ -965,30 +963,9 @@ "src/BuildingBlocks/ArcForges.Security/Tests/CapabilityEnforcementWorld.cs", "eng/policy/dependency-reviews/plt-57-r1.json", "eng/policy/dependency-reviews/plt-25-r1.json", - "native/arcpdf-abi/CMakeLists.txt", - "native/arcpdf-abi/exports/linux.map", - "native/arcpdf-abi/exports/macos.exports", - "native/arcpdf-abi/exports/windows.def", - "native/arcpdf-abi/include/arc/arc_pdf_abi.h", - "native/arcpdf-abi/src/arcpdf_abi.cpp", - "native/arcpdf-abi/src/backend_none.cpp", - "native/arcpdf-abi/src/pdf_backend.hpp", - "native/arcpdf-abi/src/pdf_engine.cpp", - "native/arcpdf-abi/tests/arcpdf_abi_engine_tests.cpp", - "native/arcpdf-abi/tests/arcpdf_abi_unsupported_tests.cpp", - "native/arcpdf-abi/tests/fake_pdf_backend.hpp", - "src/DesktopHelpers/ArcForges.ContentSandbox/Host/NativePdfParser.cs", "src/DesktopHelpers/ArcForges.ContentSandbox/Host/ProductionParserProfile.cs", - "src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ProductionPdfCompositionTests.cs", - "src/Native/ArcForges.Native.Pdf/ArcForges.Native.Pdf.csproj", - "src/Native/ArcForges.Native.Pdf/PdfAbi.cs", - "src/Native/ArcForges.Native.Pdf/PdfDocument.cs", - "src/Native/ArcForges.Native.Pdf/PdfTextJson.cs", - "src/Native/ArcForges.Native.Pdf/PdfTypes.cs", - "src/Native/ArcForges.Native.Pdf/README.md", - "src/Native/ArcForges.Native.Pdf/packages.lock.json", "eng/policy/dependency-reviews/nat-14-r1.json", - "native/arcpdf-abi/README.md" + "eng/policy/dependency-reviews/nat-32-r1.json" ], "reused": { "LICENSE": "canonical-agpl-legal-r1", diff --git a/eng/stage_integration/test_stage_integration.py b/eng/stage_integration/test_stage_integration.py index 52ab326d..48ba241d 100644 --- a/eng/stage_integration/test_stage_integration.py +++ b/eng/stage_integration/test_stage_integration.py @@ -545,8 +545,9 @@ def test_only_published_metadata_is_selected_and_nothing_is_cloned(self): def test_snapshot_building_is_deterministic(self): files = world_files() commits = {name: 'd' * 40 for name in ORDER} - first = si.build_snapshot(FakeProvider(files), commits, POLICY) - second = si.build_snapshot(FakeProvider(files), commits, POLICY) + with mock.patch.object(si, 'today_default', return_value=TODAY): + first = si.build_snapshot(FakeProvider(files), commits, POLICY) + second = si.build_snapshot(FakeProvider(files), commits, POLICY) self.assertEqual(si.dump(first), si.dump(second)) self.assertEqual(si.evaluate(first, POLICY, TODAY), []) diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index 9db191dd..76b9e2fe 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -43,7 +43,6 @@ endfunction() if(ARCFORGES_NATIVE_PROFILE STREQUAL "shim-static") find_package(OpenImageIO CONFIG REQUIRED) add_subdirectory(arcimage-abi) - add_subdirectory(arcpdf-abi) if(ARCFORGES_BUILD_TESTS) find_package(Threads REQUIRED) diff --git a/native/arcpdf-abi/CMakeLists.txt b/native/arcpdf-abi/CMakeLists.txt deleted file mode 100644 index 3e0b207e..00000000 --- a/native/arcpdf-abi/CMakeLists.txt +++ /dev/null @@ -1,78 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -set(ARCFORGES_SPDX_LICENSE "AGPL-3.0-only") -set(ARCFORGES_LICENCE_BOUNDARY "AGPL") - -# The owned arc_pdf_* engine behind a backend interface (src/pdf_backend.hpp). This delivery links NO PDF parser: -# backend_none.cpp makes arc_pdf_open fail closed with ARC_UNSUPPORTED. The PDFium binding replaces that one source -# in a later, separately admitted change; nothing here downloads, builds or vendors PDFium. -set(ARCPDF_ENGINE_SOURCES - "${CMAKE_CURRENT_SOURCE_DIR}/src/pdf_engine.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/arcpdf_abi.cpp" - ${ARCFORGES_SHARED_ABI_SOURCE}) - -add_library(arcpdf_abi SHARED ${ARCPDF_ENGINE_SOURCES} "${CMAKE_CURRENT_SOURCE_DIR}/src/backend_none.cpp") -arcforges_declare_target_licence(arcpdf_abi) -add_dependencies(arcpdf_abi arcforges_build_identity) -target_compile_features(arcpdf_abi PRIVATE cxx_std_20) -set_target_properties(arcpdf_abi PROPERTIES - CXX_EXTENSIONS OFF - CXX_VISIBILITY_PRESET hidden - VISIBILITY_INLINES_HIDDEN YES - OUTPUT_NAME "ArcPdfNative") -target_include_directories(arcpdf_abi - PUBLIC - "${CMAKE_CURRENT_SOURCE_DIR}/include" - "${CMAKE_CURRENT_SOURCE_DIR}/../shared/include" - PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}/src" - "${ARCFORGES_SHARED_ABI_PRIVATE_INCLUDE}") - -if(MSVC) - target_compile_options(arcpdf_abi PRIVATE /W4 /WX /permissive- /EHsc /GR-) - target_link_options(arcpdf_abi PRIVATE "/DEF:${CMAKE_CURRENT_SOURCE_DIR}/exports/windows.def") -elseif(APPLE) - target_compile_options(arcpdf_abi PRIVATE -Wall -Wextra -Wpedantic -Werror -fno-rtti) - target_link_options(arcpdf_abi PRIVATE "-Wl,-exported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/exports/macos.exports") -else() - target_compile_options(arcpdf_abi PRIVATE -Wall -Wextra -Wpedantic -Werror -fno-rtti) - target_link_options(arcpdf_abi PRIVATE "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/exports/linux.map") -endif() - -install(TARGETS arcpdf_abi - RUNTIME DESTINATION native - LIBRARY DESTINATION native - ARCHIVE DESTINATION lib) - -if(ARCFORGES_BUILD_TESTS) - find_package(Threads REQUIRED) - - # The engine compiled together with the scripted fake backend (test only; the fake is never part of arcpdf_abi). - add_executable(arcpdf_abi_engine_tests - "tests/arcpdf_abi_engine_tests.cpp" - ${ARCPDF_ENGINE_SOURCES}) - arcforges_declare_target_licence(arcpdf_abi_engine_tests) - set_target_properties(arcpdf_abi_engine_tests PROPERTIES ARCFORGES_TEST_ONLY TRUE) - add_dependencies(arcpdf_abi_engine_tests arcforges_build_identity) - target_compile_features(arcpdf_abi_engine_tests PRIVATE cxx_std_20) - target_include_directories(arcpdf_abi_engine_tests PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}/include" - "${CMAKE_CURRENT_SOURCE_DIR}/src" - "${CMAKE_CURRENT_SOURCE_DIR}/tests" - "${CMAKE_CURRENT_SOURCE_DIR}/../shared/include" - "${ARCFORGES_SHARED_ABI_PRIVATE_INCLUDE}") - target_link_libraries(arcpdf_abi_engine_tests PRIVATE Threads::Threads) - if(MSVC) - target_compile_options(arcpdf_abi_engine_tests PRIVATE /W4 /WX /permissive- /EHsc /GR-) - else() - target_compile_options(arcpdf_abi_engine_tests PRIVATE -Wall -Wextra -Wpedantic -Werror -fno-rtti) - endif() - add_test(NAME arcpdf_abi_engine_tests COMMAND arcpdf_abi_engine_tests) - - # The shipped configuration: the real library target with no parser linked. - add_executable(arcpdf_abi_unsupported_tests "tests/arcpdf_abi_unsupported_tests.cpp") - arcforges_declare_target_licence(arcpdf_abi_unsupported_tests) - set_target_properties(arcpdf_abi_unsupported_tests PROPERTIES ARCFORGES_TEST_ONLY TRUE) - target_compile_features(arcpdf_abi_unsupported_tests PRIVATE cxx_std_20) - target_link_libraries(arcpdf_abi_unsupported_tests PRIVATE arcpdf_abi) - add_test(NAME arcpdf_abi_unsupported_tests COMMAND arcpdf_abi_unsupported_tests) -endif() diff --git a/native/arcpdf-abi/README.md b/native/arcpdf-abi/README.md deleted file mode 100644 index ed9a1a2e..00000000 --- a/native/arcpdf-abi/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# ArcPdfNative - -The owned `arc_pdf_*` C17 ABI (annex 06) for bounded PDF page geometry, text with geometry and RGBA8 tile rendering. It is -loaded only inside the ContentSandbox helper process (`src/DesktopHelpers/ArcForges.ContentSandbox`); a product process never -parses PDF bytes. - -## What is here - -- `src/pdf_engine.cpp`: the whole exported surface. It validates every argument and every value a backend returns, applies the - signed hard limits (input, memory, output, dimensions, items, deadline), serialises each document (PDFium is not thread - safe), keeps opaque generation handles (at most 64 open), honours cancellation and the call deadline, encodes text as the - closed JSON of annex 06 and never leaves partial pixels in a caller buffer after a failure. -- `src/pdf_backend.hpp`: the seam to whatever parses PDF bytes (`backend`, `document`, `byte_source`). Scripts and actions are - disabled by contract. A backend is never trusted: it may throw, hang, return non-finite geometry or boxes outside the text - and the engine fails the call closed. -- `src/backend_none.cpp`: **the only backend this build links is none.** `arc_pdf_open` returns `ARC_UNSUPPORTED`, outputs stay - zero, and `arc_pdf_get_build_info` reports `backend=none`. There is no fallback parser. The PDFium (chromium/8044) binding, - its source/licence admission and the `Runtime.` packages are separate, later work; nothing here downloads, builds or - vendors PDFium, and the library is not an admitted package. -- `tests/`: `arcpdf_abi_engine_tests` compiles the engine with a scripted fake backend (`tests/fake_pdf_backend.hpp`, TEST ONLY, - never part of the library target) and drives it with hostile behaviour; `arcpdf_abi_unsupported_tests` links the real library - target and checks the fail-closed configuration. Both are CTest targets run by `native-abi.yml`. They prove the engine, not - PDFium. - -## Semantics fixed by this library - -- `arc_pdf_text`: `start` and `count` are UTF-16 code-unit offsets into the full page text, at most 65536 units per call. A - chunk never splits a surrogate pair; a start inside a pair is refused. Box offsets are absolute offsets into the page text and - a chunk carries only the boxes wholly inside it. A box that would straddle the chunk end moves whole into the next chunk (the - chunk ends where the box starts); only a box that already straddles the requested start (the caller started inside it) is not - carried. At most `max_items` boxes per chunk. Lone surrogates are replaced by U+FFFD in the UTF-8 output. -- `arc_pdf_render`: the page record must equal the geometry the document reports; the tile lies inside the full pixel grid; the - required size is `(height - 1) * stride + width * 4`; grids above 268435456 pixels or tiles above 64 MiB are refused. -- `arc_pdf_open`: the `arc_io_v1` callbacks and context must stay valid until `arc_pdf_close`. A password that is required but - missing or wrong is `ARC_PERMISSION_DENIED`. -- `arc_io_v1.read_at`: the engine clamps every request to the input length and treats any short read (including a zero-length - OK read) as `ARC_IO`: a short read is allowed only if the callback reports the end of the input, which the clamped window - already excludes. - -## Obligations this library leaves to the real backend (NAT.15) - -- A real backend must read the input in a loop with a progress guard and must call `call_context::check()` regularly. Deadlines - and cancellation are cooperative here: a backend that never checks is bounded only by the helper's own deadline and the - operating-system containment, and `arc_pdf_close` blocks until a running call returns. -- A box that spans more than a whole chunk window, or that begins before a caller-chosen start, is not carried by any chunk. -- Scripts and actions must be disabled in the real library, fonts must not be read from the file system inside the sandbox, and - the real library must be proven under hostile PDFs (malformed, crashing, hanging) inside the real containment. - -## Sanitizer check - -The engine test passes unmodified under MSVC AddressSanitizer (`/fsanitize=address`, debug). A first ASan run showed six -failures that were a test defect, not an engine defect: the concurrent-close test legitimately saw `ARC_CLOSED` between the size -query and the fetch of `arc_pdf_text` when ASan slowed the worker threads, and the helper then checked the response size on a -failed call. The test helper now checks it only on success. No ASan or UBSan job is wired into CI (a hosted sanitizer job is a -follow-up). diff --git a/native/arcpdf-abi/exports/linux.map b/native/arcpdf-abi/exports/linux.map deleted file mode 100644 index 29d8747b..00000000 --- a/native/arcpdf-abi/exports/linux.map +++ /dev/null @@ -1,13 +0,0 @@ -/* SPDX-License-Identifier: AGPL-3.0-only */ -ARCFORGES_1.1 { - global: - arc_pdf_get_abi_version; - arc_pdf_get_build_info; - arc_pdf_get_last_error; - arc_pdf_open; - arc_pdf_page_info; - arc_pdf_render; - arc_pdf_text; - arc_pdf_close; - local: *; -}; diff --git a/native/arcpdf-abi/exports/macos.exports b/native/arcpdf-abi/exports/macos.exports deleted file mode 100644 index e73a10a0..00000000 --- a/native/arcpdf-abi/exports/macos.exports +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -_arc_pdf_get_abi_version -_arc_pdf_get_build_info -_arc_pdf_get_last_error -_arc_pdf_open -_arc_pdf_page_info -_arc_pdf_render -_arc_pdf_text -_arc_pdf_close diff --git a/native/arcpdf-abi/exports/windows.def b/native/arcpdf-abi/exports/windows.def deleted file mode 100644 index 30a56dc3..00000000 --- a/native/arcpdf-abi/exports/windows.def +++ /dev/null @@ -1,11 +0,0 @@ -; SPDX-License-Identifier: AGPL-3.0-only -LIBRARY "ArcPdfNative" -EXPORTS - arc_pdf_get_abi_version - arc_pdf_get_build_info - arc_pdf_get_last_error - arc_pdf_open - arc_pdf_page_info - arc_pdf_render - arc_pdf_text - arc_pdf_close diff --git a/native/arcpdf-abi/include/arc/arc_pdf_abi.h b/native/arcpdf-abi/include/arc/arc_pdf_abi.h deleted file mode 100644 index c01d9d1c..00000000 --- a/native/arcpdf-abi/include/arc/arc_pdf_abi.h +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -#ifndef ARC_PDF_ABI_H -#define ARC_PDF_ABI_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* The library preamble. The five functional arc_pdf_* exports are declared in arc_native_abi.h (ABI 1.1). */ -ARC_ABI_EXPORT arc_status_t ARC_ABI_CALL arc_pdf_get_abi_version(uint32_t* out_major, uint32_t* out_minor); -ARC_ABI_EXPORT arc_status_t ARC_ABI_CALL arc_pdf_get_build_info(arc_mut_buffer_t* out_utf8); -ARC_ABI_EXPORT arc_status_t ARC_ABI_CALL arc_pdf_get_last_error(arc_error_info_t* out_error); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/native/arcpdf-abi/src/arcpdf_abi.cpp b/native/arcpdf-abi/src/arcpdf_abi.cpp deleted file mode 100644 index 7c5e6ffd..00000000 --- a/native/arcpdf-abi/src/arcpdf_abi.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -#include - -#include "arc_native_abi_internal.hpp" -#include "pdf_backend.hpp" - -arc_status_t ARC_ABI_CALL arc_pdf_get_abi_version(uint32_t* out_major, uint32_t* out_minor) -{ - if (out_major == nullptr || out_minor == nullptr) { - return arc::abi::fail(ARC_INVALID_ARGUMENT, "ABI version outputs are required", arc::pdf::pdf_domain); - } - *out_major = ARC_NATIVE_ABI_MAJOR; - *out_minor = ARC_NATIVE_FUNCTIONAL_ABI_MINOR; // this library ships the functional exports - return ARC_OK; -} - -arc_status_t ARC_ABI_CALL arc_pdf_get_build_info(arc_mut_buffer_t* out_utf8) -{ - try { - // The backend is named so a consumer can tell a parserless build from one with a parser linked. - return arc::abi::write_build_info(arc::pdf::linked_backend() ? "ArcPdfNative;abi=1.1;backend=linked" - : "ArcPdfNative;abi=1.1;backend=none", - out_utf8); - } catch (...) { - return arc::abi::fail(ARC_INTERNAL, "PDF build information failed", arc::pdf::pdf_domain); - } -} - -arc_status_t ARC_ABI_CALL arc_pdf_get_last_error(arc_error_info_t* out_error) -{ - return arc::abi::get_last_error(out_error); -} diff --git a/native/arcpdf-abi/src/backend_none.cpp b/native/arcpdf-abi/src/backend_none.cpp deleted file mode 100644 index 0900c987..00000000 --- a/native/arcpdf-abi/src/backend_none.cpp +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -#include "pdf_backend.hpp" - -// No PDF parser is linked into this build. arc_pdf_open then fails closed with ARC_UNSUPPORTED; the library never -// substitutes a fallback parser. The PDFium binding replaces this translation unit and is not part of this delivery. -std::shared_ptr arc::pdf::linked_backend() -{ - return nullptr; -} diff --git a/native/arcpdf-abi/src/pdf_backend.hpp b/native/arcpdf-abi/src/pdf_backend.hpp deleted file mode 100644 index 5547032f..00000000 --- a/native/arcpdf-abi/src/pdf_backend.hpp +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -#ifndef ARC_PDF_BACKEND_HPP -#define ARC_PDF_BACKEND_HPP - -#include - -#include -#include -#include -#include -#include -#include - -// The seam between the owned arc_pdf_* engine (limits, validation, handles, cancellation, output encoding) and -// whatever parses PDF bytes. The engine never trusts a backend: every value a backend returns is validated before -// it reaches a caller, and a backend that throws, hangs past its deadline or returns nonsense fails the call closed. -// The only production backend is the PDFium binding; until it is linked the library reports ARC_UNSUPPORTED. -namespace arc::pdf { - -constexpr uint32_t pdf_domain = 0x50444655U; - -// Hard producer profile. Caller limits can lower these, never raise them. -constexpr uint64_t profile_max_input_bytes = UINT64_C(256) * 1024 * 1024; -constexpr uint64_t profile_max_memory_bytes = UINT64_C(1) * 1024 * 1024 * 1024; -constexpr uint64_t profile_max_output_bytes = UINT64_C(3) * 64 * 1024 * 1024; -constexpr uint32_t profile_max_dimension = 65535; -constexpr uint32_t profile_max_items = 1024; -constexpr uint32_t profile_max_timeout_ms = 30000; -constexpr uint64_t profile_max_pixels = UINT64_C(268435456); -constexpr uint64_t profile_max_tile_bytes = UINT64_C(64) * 1024 * 1024; -constexpr uint32_t max_text_units_per_call = 65536; -constexpr uint32_t max_password_bytes = 1024; -constexpr uint32_t max_pages = UINT32_C(1) << 24; -constexpr uint32_t max_page_text_units = UINT32_C(16) * 1024 * 1024; -constexpr double max_page_points = 1000000.0; - -struct page_geometry final { - uint32_t rotation = 0; - double width_points = 0; - double height_points = 0; -}; - -// Offsets are UTF-16 code-unit offsets into the full page text. -struct text_box final { - uint32_t start = 0; - uint32_t length = 0; - double x = 0; - double y = 0; - double width = 0; - double height = 0; -}; - -struct page_text final { - std::u16string text; - std::vector boxes; -}; - -struct region final { - uint32_t x = 0; - uint32_t y = 0; - uint32_t width = 0; - uint32_t height = 0; -}; - -// What every backend call runs under: the validated limits, the caller's cancellation and the call deadline. -class call_context final { - public: - call_context(const arc_limits_v1& limits, const arc_cancel_token_t* cancel) noexcept; - [[nodiscard]] const arc_limits_v1& limits() const noexcept - { - return limits_; - } - // ARC_OK, ARC_CANCELLED, ARC_RESOURCE_LIMIT (deadline) or ARC_INVALID_ARGUMENT/ARC_CORRUPT for a bad token. - [[nodiscard]] arc_status_t check() const noexcept; - - private: - arc_limits_v1 limits_; - const arc_cancel_token_t* cancel_; - std::chrono::steady_clock::time_point deadline_; -}; - -// The immutable input, as read through the caller's arc_io_v1 callbacks. Reads never run past the length. -class byte_source { - public: - virtual ~byte_source() = default; - [[nodiscard]] virtual uint64_t length() const noexcept = 0; - // Short reads happen only at the end of the input; done is always <= requested. - virtual arc_status_t read_at(uint64_t offset, void* destination, uint64_t requested, uint64_t* done) noexcept = 0; -}; - -class document { - public: - virtual ~document() = default; - virtual arc_status_t geometry(uint32_t index, const call_context& context, page_geometry& out) = 0; - virtual arc_status_t text(uint32_t index, const call_context& context, page_text& out) = 0; - // Writes tile rows into out at the given row stride. The region lies inside the full pixel grid of the page. - virtual arc_status_t render(uint32_t index, uint32_t full_width, uint32_t full_height, const region& tile, - uint64_t row_stride, uint8_t* out, uint64_t out_size, const call_context& context) = 0; -}; - -class backend { - public: - virtual ~backend() = default; - // Opens the document with scripts and actions disabled. A password is only passed when the caller gave one. - // ARC_PERMISSION_DENIED: a password is required or wrong. ARC_CORRUPT: not a usable PDF. - virtual arc_status_t open(std::shared_ptr source, std::string_view password, - const call_context& context, std::unique_ptr& out, uint32_t& pages) = 0; -}; - -// The backend this build links, or null. backend_none.cpp returns null; the PDFium binding provides the real one. -std::shared_ptr linked_backend(); - -} // namespace arc::pdf - -#endif diff --git a/native/arcpdf-abi/src/pdf_engine.cpp b/native/arcpdf-abi/src/pdf_engine.cpp deleted file mode 100644 index deedec56..00000000 --- a/native/arcpdf-abi/src/pdf_engine.cpp +++ /dev/null @@ -1,612 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// The owned arc_pdf_* engine. It validates every argument and every value a PDF backend returns, enforces the -// signed hard limits, serialises each document (PDFium is not thread safe), encodes bounded text/geometry and -// pixel output, and fails closed: an output buffer the caller supplied is never left holding partial results. -#include - -#include "arc_native_abi_internal.hpp" -#include "pdf_backend.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace arc::pdf { -namespace { - -constexpr uint32_t document_kind = 1; -constexpr size_t geometry_cache_limit = 1024; - -arc_status_t failure(arc_status_t status, std::string_view message) noexcept -{ - return abi::fail(status, message, pdf_domain); -} - -// A backend answers with ARC_OK or one of the closed negative codes. Anything else is a defect of the backend. -arc_status_t backend_status(arc_status_t status, std::string_view message) noexcept -{ - if (status == ARC_OK) { - return ARC_OK; - } - if (status >= ARC_INTERNAL && status <= ARC_INVALID_ARGUMENT) { - return failure(status, message); - } - return failure(ARC_INTERNAL, "The PDF backend returned a status outside the closed set"); -} - -template arc_status_t guarded(F&& body) noexcept -{ - try { - return body(); - } catch (const std::bad_alloc&) { - return failure(ARC_OUT_OF_MEMORY, "PDF allocation failed"); - } catch (...) { - return failure(ARC_INTERNAL, "The PDF backend raised an exception"); - } -} - -arc_status_t take_limits(const arc_limits_v1* in, arc_limits_v1& out) noexcept -{ - if (in == nullptr) { - return failure(ARC_INVALID_ARGUMENT, "Limits are required"); - } - const arc_status_t valid = abi::validate_record(in, sizeof(arc_limits_v1)); - if (valid != ARC_OK) { - return valid; - } - std::memcpy(&out, in, sizeof(arc_limits_v1)); - if (out.max_input_bytes == 0 || out.max_memory_bytes == 0 || out.max_output_bytes == 0 || out.max_width == 0 || - out.max_height == 0 || out.max_items == 0 || out.timeout_ms == 0) { - return failure(ARC_INVALID_ARGUMENT, "Every limit must be positive"); - } - if (out.max_input_bytes > profile_max_input_bytes || out.max_memory_bytes > profile_max_memory_bytes || - out.max_output_bytes > profile_max_output_bytes || out.max_width > profile_max_dimension || - out.max_height > profile_max_dimension || out.max_items > profile_max_items || - out.timeout_ms > profile_max_timeout_ms) { - return failure(ARC_RESOURCE_LIMIT, "A limit exceeds the signed producer profile"); - } - return ARC_OK; -} - -class io_source final : public byte_source { - public: - explicit io_source(const arc_io_v1& io) noexcept : io_(io) - { - } - [[nodiscard]] uint64_t length() const noexcept override - { - return io_.length; - } - arc_status_t read_at(uint64_t offset, void* destination, uint64_t requested, uint64_t* done) noexcept override - { - if (done == nullptr || (destination == nullptr && requested != 0)) { - return ARC_INVALID_ARGUMENT; - } - *done = 0; - if (offset >= io_.length || requested == 0) { - return ARC_OK; - } - const uint64_t count = std::min(requested, io_.length - offset); - uint64_t read = 0; - const arc_status_t status = io_.read_at(io_.context, offset, destination, count, &read); - if (status != ARC_OK && status != ARC_END_OF_STREAM) { - return status < 0 ? status : ARC_IO; - } - // The window is already clamped to the input, so a short read, however it is reported, is a defect of the - // callback. - if (read != count) { - return ARC_IO; - } - *done = read; - return ARC_OK; - } - - private: - arc_io_v1 io_; -}; - -struct document_state final { - std::shared_ptr source; - std::unique_ptr parsed; - arc_limits_v1 limits{}; - uint32_t pages = 0; - std::mutex gate; - std::map geometries; - uint32_t cached_text_page = UINT32_MAX; - page_text cached_text; -}; - -abi::handle_table& table() -{ - static abi::handle_table instance(64); - return instance; -} - -bool is_high(char16_t unit) noexcept -{ - return unit >= 0xD800 && unit <= 0xDBFF; -} - -bool is_low(char16_t unit) noexcept -{ - return unit >= 0xDC00 && unit <= 0xDFFF; -} - -bool finite_in(double value, double low, double high) noexcept -{ - return std::isfinite(value) && value >= low && value <= high; -} - -// Caller holds state.gate. -arc_status_t geometry_of(document_state& state, uint32_t index, const call_context& context, page_geometry& out) -{ - if (index >= state.pages) { - return failure(ARC_NOT_FOUND, "Page index is out of range"); - } - if (const auto found = state.geometries.find(index); found != state.geometries.end()) { - out = found->second; - return ARC_OK; - } - if (const arc_status_t gate = context.check(); gate != ARC_OK) { - return gate; - } - page_geometry value; - const arc_status_t status = - backend_status(state.parsed->geometry(index, context, value), "PDF page geometry failed"); - if (status != ARC_OK) { - return status; - } - if (const arc_status_t gate = context.check(); gate != ARC_OK) { - return gate; - } - const bool rotation_valid = - value.rotation == 0 || value.rotation == 90 || value.rotation == 180 || value.rotation == 270; - if (!rotation_valid || !finite_in(value.width_points, std::numeric_limits::min(), max_page_points) || - !finite_in(value.height_points, std::numeric_limits::min(), max_page_points)) { - return failure(ARC_CORRUPT, "The PDF backend returned invalid page geometry"); - } - if (state.geometries.size() < geometry_cache_limit) { - state.geometries.emplace(index, value); - } - out = value; - return ARC_OK; -} - -// Caller holds state.gate. Validates once on fetch: finite boxes inside the text, ordered by start. -arc_status_t text_of(document_state& state, uint32_t index, const call_context& context, const page_text*& out) -{ - if (index >= state.pages) { - return failure(ARC_NOT_FOUND, "Page index is out of range"); - } - if (state.cached_text_page == index) { - out = &state.cached_text; - return ARC_OK; - } - if (const arc_status_t gate = context.check(); gate != ARC_OK) { - return gate; - } - page_text fetched; - const arc_status_t status = - backend_status(state.parsed->text(index, context, fetched), "PDF text extraction failed"); - if (status != ARC_OK) { - return status; - } - if (const arc_status_t gate = context.check(); gate != ARC_OK) { - return gate; - } - if (fetched.text.size() > max_page_text_units) { - return failure(ARC_RESOURCE_LIMIT, "Page text exceeds its bound"); - } - const uint64_t length = fetched.text.size(); - uint32_t previous = 0; - for (const text_box& box : fetched.boxes) { - if (box.start < previous || box.length == 0 || static_cast(box.start) + box.length > length || - !finite_in(box.x, -max_page_points, max_page_points) || - !finite_in(box.y, -max_page_points, max_page_points) || !finite_in(box.width, 0.0, max_page_points) || - !finite_in(box.height, 0.0, max_page_points)) { - return failure(ARC_CORRUPT, "The PDF backend returned an invalid text box"); - } - previous = box.start; - } - state.cached_text = std::move(fetched); - state.cached_text_page = index; - out = &state.cached_text; - return ARC_OK; -} - -void append_number(std::string& json, double value) -{ - char buffer[40]; - const auto result = std::to_chars(buffer, buffer + sizeof(buffer), value); - json.append(buffer, result.ptr); -} - -void append_number(std::string& json, uint64_t value) -{ - char buffer[24]; - const auto result = std::to_chars(buffer, buffer + sizeof(buffer), value); - json.append(buffer, result.ptr); -} - -void append_utf8(std::string& json, char32_t scalar) -{ - if (scalar < 0x80) { - json.push_back(static_cast(scalar)); - } else if (scalar < 0x800) { - json.push_back(static_cast(0xC0 | (scalar >> 6))); - json.push_back(static_cast(0x80 | (scalar & 0x3F))); - } else if (scalar < 0x10000) { - json.push_back(static_cast(0xE0 | (scalar >> 12))); - json.push_back(static_cast(0x80 | ((scalar >> 6) & 0x3F))); - json.push_back(static_cast(0x80 | (scalar & 0x3F))); - } else { - json.push_back(static_cast(0xF0 | (scalar >> 18))); - json.push_back(static_cast(0x80 | ((scalar >> 12) & 0x3F))); - json.push_back(static_cast(0x80 | ((scalar >> 6) & 0x3F))); - json.push_back(static_cast(0x80 | (scalar & 0x3F))); - } -} - -void append_json_text(std::string& json, const std::u16string& text, size_t begin, size_t end) -{ - static constexpr char hex[] = "0123456789abcdef"; - json.push_back('"'); - for (size_t i = begin; i < end; ++i) { - const char16_t unit = text[i]; - if (unit == u'"' || unit == u'\\') { - json.push_back('\\'); - json.push_back(static_cast(unit)); - } else if (unit < 0x20) { - json.append("\\u00"); - json.push_back(hex[(unit >> 4) & 0xF]); - json.push_back(hex[unit & 0xF]); - } else if (is_high(unit) && i + 1 < end && is_low(text[i + 1])) { - append_utf8(json, 0x10000 + ((static_cast(unit) - 0xD800) << 10) + - (static_cast(text[i + 1]) - 0xDC00)); - ++i; - } else if (is_high(unit) || is_low(unit)) { - append_utf8(json, 0xFFFD); // A lone surrogate is never valid UTF-8; it is replaced, not passed on. - } else { - append_utf8(json, unit); - } - } - json.push_back('"'); -} - -size_t keep_pairs_whole(const std::u16string& text, size_t start, size_t end) noexcept -{ - if (end < text.size() && end > start && is_low(text[end]) && is_high(text[end - 1])) { - --end; - } - if (end == start && start < text.size()) { - end = start + ((is_high(text[start]) && start + 1 < text.size() && is_low(text[start + 1])) ? 2 : 1); - } - return end; -} - -} // namespace - -call_context::call_context(const arc_limits_v1& limits, const arc_cancel_token_t* cancel) noexcept - : limits_(limits), cancel_(cancel), - deadline_(std::chrono::steady_clock::now() + std::chrono::milliseconds(limits.timeout_ms)) -{ -} - -arc_status_t call_context::check() const noexcept -{ - if (const arc_status_t cancelled = abi::check_cancelled(cancel_); cancelled != ARC_OK) { - return cancelled; - } - if (std::chrono::steady_clock::now() > deadline_) { - return failure(ARC_RESOURCE_LIMIT, "The parser deadline passed"); - } - return ARC_OK; -} - -} // namespace arc::pdf - -using namespace arc::pdf; - -arc_status_t ARC_ABI_CALL arc_pdf_open(const arc_io_v1* io, arc_string_view_t password, const arc_limits_v1* limits, - arc_handle_t* document, uint32_t* pages, const arc_cancel_token_t* cancel) -{ - return guarded([&]() -> arc_status_t { - if (const arc_status_t prepared = arc::abi::prepare_handle_output(document); prepared != ARC_OK) { - return prepared; - } - if (pages == nullptr) { - return failure(ARC_INVALID_ARGUMENT, "Page-count output is required"); - } - *pages = 0; - if (const arc_status_t valid = arc::abi::validate_record(io, sizeof(arc_io_v1)); valid != ARC_OK) { - return valid; - } - if (io->read_at == nullptr) { - return failure(ARC_INVALID_ARGUMENT, "The input read callback is required"); - } - arc_limits_v1 bounded{}; - if (const arc_status_t taken = take_limits(limits, bounded); taken != ARC_OK) { - return taken; - } - if (io->length == 0) { - return failure(ARC_CORRUPT, "The input is empty"); - } - if (io->length > bounded.max_input_bytes || (io->max_length != 0 && io->length > io->max_length)) { - return failure(ARC_RESOURCE_LIMIT, "The input exceeds its bound"); - } - if ((password.data == nullptr && password.size != 0) || password.size > max_password_bytes) { - return failure(ARC_INVALID_ARGUMENT, "The password is invalid"); - } - if (const arc_status_t cancelled = arc::abi::check_cancelled(cancel); cancelled != ARC_OK) { - return cancelled; - } - const std::shared_ptr parser = linked_backend(); - if (!parser) { - return failure(ARC_UNSUPPORTED, "No PDF backend is linked into this build"); - } - - const call_context context(bounded, cancel); - auto state = std::make_shared(); - state->source = std::make_shared(*io); - state->limits = bounded; - uint32_t count = 0; - const std::string_view secret(password.data == nullptr ? "" : password.data, - static_cast(password.size)); - const arc_status_t opened = backend_status(parser->open(state->source, secret, context, state->parsed, count), - "The PDF could not be opened"); - if (opened != ARC_OK) { - return opened; - } - if (!state->parsed) { - return failure(ARC_INTERNAL, "The PDF backend returned no document"); - } - if (const arc_status_t gate = context.check(); gate != ARC_OK) { - return gate; - } - if (count == 0) { - return failure(ARC_CORRUPT, "The PDF has no pages"); - } - if (count > max_pages) { - return failure(ARC_RESOURCE_LIMIT, "The PDF has too many pages"); - } - state->pages = count; - arc_handle_t created = 0; - if (const arc_status_t made = table().create(document_kind, state, &created); made != ARC_OK) { - return made; - } - *document = created; - *pages = count; - return ARC_OK; - }); -} - -arc_status_t ARC_ABI_CALL arc_pdf_page_info(arc_handle_t document, uint32_t index, arc_pdf_page_v1* page) -{ - return guarded([&]() -> arc_status_t { - if (const arc_status_t valid = arc::abi::validate_record(page, sizeof(arc_pdf_page_v1)); valid != ARC_OK) { - return valid; - } - const uint32_t size = page->struct_size; - const uint32_t version = page->struct_version; - std::memset(page, 0, sizeof(arc_pdf_page_v1)); - page->struct_size = size; - page->struct_version = version; - arc::abi::handle_lease lease; - if (const arc_status_t acquired = table().acquire(document, document_kind, &lease); acquired != ARC_OK) { - return acquired; - } - auto& state = *lease.as(); - const std::lock_guard lock(state.gate); - const call_context context(state.limits, nullptr); - page_geometry value; - if (const arc_status_t read = geometry_of(state, index, context, value); read != ARC_OK) { - return read; - } - page->page_index = index; - page->rotation = value.rotation; - page->width_points = value.width_points; - page->height_points = value.height_points; - return ARC_OK; - }); -} - -arc_status_t ARC_ABI_CALL arc_pdf_render(arc_handle_t document, const arc_pdf_page_v1* page, const arc_region_v1* tile, - uint32_t full_width, uint32_t full_height, arc_mut_buffer_t* rgba8, - const arc_cancel_token_t* cancel) -{ - uint8_t* written_from = nullptr; - uint64_t written_size = 0; - const arc_status_t status = guarded([&]() -> arc_status_t { - if (const arc_status_t valid = arc::abi::validate_record(page, sizeof(arc_pdf_page_v1)); valid != ARC_OK) { - return valid; - } - if (const arc_status_t valid = arc::abi::validate_record(tile, sizeof(arc_region_v1)); valid != ARC_OK) { - return valid; - } - if (rgba8 == nullptr) { - return failure(ARC_INVALID_ARGUMENT, "Pixel output is required"); - } - if (tile->width == 0 || tile->height == 0 || full_width == 0 || full_height == 0 || tile->first_sample != 0 || - tile->sample_count != 0) { - return failure(ARC_INVALID_ARGUMENT, "The render region is invalid"); - } - const uint64_t row_bytes = static_cast(tile->width) * 4U; - const uint64_t stride = tile->row_stride == 0 ? row_bytes : tile->row_stride; - if (stride < row_bytes || stride > profile_max_tile_bytes) { - return failure(ARC_INVALID_ARGUMENT, "The row stride is invalid"); - } - if (static_cast(tile->x) + tile->width > full_width || - static_cast(tile->y) + tile->height > full_height) { - return failure(ARC_INVALID_ARGUMENT, "The region lies outside the pixel grid"); - } - if (static_cast(full_width) * full_height > profile_max_pixels) { - return failure(ARC_RESOURCE_LIMIT, "The pixel grid exceeds its bound"); - } - // (height - 1) rows of stride plus one packed row: no byte past the last pixel is written or required. - const uint64_t required = (static_cast(tile->height) - 1U) * stride + row_bytes; - if (const arc_status_t cancelled = arc::abi::check_cancelled(cancel); cancelled != ARC_OK) { - return cancelled; - } - arc::abi::handle_lease lease; - if (const arc_status_t acquired = table().acquire(document, document_kind, &lease); acquired != ARC_OK) { - return acquired; - } - auto& state = *lease.as(); - if (full_width > state.limits.max_width || full_height > state.limits.max_height || - required > state.limits.max_output_bytes || required > profile_max_tile_bytes) { - return failure(ARC_RESOURCE_LIMIT, "The render request exceeds the document limits"); - } - if (const arc_status_t prepared = arc::abi::prepare_buffer_output(rgba8, required); prepared != ARC_OK) { - return prepared; - } - const std::lock_guard lock(state.gate); - const call_context context(state.limits, cancel); - page_geometry known; - if (const arc_status_t read = geometry_of(state, page->page_index, context, known); read != ARC_OK) { - return read; - } - // Scaling is never inferred from the caller's page record: it must be the geometry this document reports. - if (page->rotation != known.rotation || page->width_points != known.width_points || - page->height_points != known.height_points) { - return failure(ARC_INVALID_ARGUMENT, "The page record differs from the document page"); - } - auto* const out = static_cast(rgba8->data); - written_from = out; - written_size = required; - const region area{tile->x, tile->y, tile->width, tile->height}; - const arc_status_t rendered = backend_status( - state.parsed->render(page->page_index, full_width, full_height, area, stride, out, required, context), - "PDF rendering failed"); - if (rendered != ARC_OK) { - return rendered; - } - return context.check(); - }); - if (status != ARC_OK && written_from != nullptr) { - std::memset(written_from, 0, static_cast(written_size)); // never leave partial pixels behind - } - return status; -} - -arc_status_t ARC_ABI_CALL arc_pdf_text(arc_handle_t document, uint32_t page, uint32_t start, uint32_t count, - arc_mut_buffer_t* text_geometry, const arc_cancel_token_t* cancel) -{ - return guarded([&]() -> arc_status_t { - if (text_geometry == nullptr) { - return failure(ARC_INVALID_ARGUMENT, "Text output is required"); - } - if (count == 0 || count > max_text_units_per_call) { - return failure(ARC_INVALID_ARGUMENT, "The text count is outside 1..65536"); - } - if (const arc_status_t cancelled = arc::abi::check_cancelled(cancel); cancelled != ARC_OK) { - return cancelled; - } - arc::abi::handle_lease lease; - if (const arc_status_t acquired = table().acquire(document, document_kind, &lease); acquired != ARC_OK) { - return acquired; - } - auto& state = *lease.as(); - const std::lock_guard lock(state.gate); - const call_context context(state.limits, cancel); - const page_text* extracted = nullptr; - if (const arc_status_t read = text_of(state, page, context, extracted); read != ARC_OK) { - return read; - } - const std::u16string& text = extracted->text; - const std::vector& boxes = extracted->boxes; - const size_t length = text.size(); - if (start > length || (start > 0 && start < length && is_low(text[start]) && is_high(text[start - 1]))) { - return failure(ARC_INVALID_ARGUMENT, "The text start is not a position a chunk can begin at"); - } - - size_t end = keep_pairs_whole(text, start, std::min(length, static_cast(start) + count)); - std::vector inside; - while (true) { - inside.clear(); - auto first = std::lower_bound(boxes.begin(), boxes.end(), start, - [](const text_box& box, uint32_t value) { return box.start < value; }); - // A box that would straddle the chunk end moves whole into the next chunk: the chunk ends where it starts. - // A box that already straddles the window start cannot be moved and is not carried (it began in an earlier - // window). - size_t moved_end = end; - for (auto probe = first; probe != boxes.end() && probe->start < end; ++probe) { - if (probe->start > start && static_cast(probe->start) + probe->length > end) { - moved_end = std::min(moved_end, keep_pairs_whole(text, start, probe->start)); - break; - } - } - if (moved_end < end) { - end = moved_end; - continue; - } - for (; first != boxes.end() && first->start < end; ++first) { - if (static_cast(first->start) + first->length <= end) { - inside.push_back(&*first); - } - } - if (inside.size() <= state.limits.max_items) { - break; - } - const size_t cut = keep_pairs_whole(text, start, inside[state.limits.max_items]->start); - if (cut <= start || cut >= end) { - return failure(ARC_RESOURCE_LIMIT, "The text chunk carries too many boxes"); - } - end = cut; - } - - std::string json; - json.reserve(256 + (end - start) * 2 + inside.size() * 96); - json.append("{\"version\":1,\"page\":"); - append_number(json, static_cast(page)); - json.append(",\"start\":"); - append_number(json, static_cast(start)); - if (end < length) { - json.append(",\"next\":"); - append_number(json, static_cast(end)); - } - json.append(",\"text\":"); - append_json_text(json, text, start, end); - json.append(",\"boxes\":["); - bool separator = false; - for (const text_box* box : inside) { - if (separator) { - json.push_back(','); - } - separator = true; - json.append("{\"start\":"); - append_number(json, static_cast(box->start)); - json.append(",\"length\":"); - append_number(json, static_cast(box->length)); - json.append(",\"x\":"); - append_number(json, box->x); - json.append(",\"y\":"); - append_number(json, box->y); - json.append(",\"width\":"); - append_number(json, box->width); - json.append(",\"height\":"); - append_number(json, box->height); - json.push_back('}'); - } - json.append("]}"); - if (json.size() > state.limits.max_output_bytes) { - return failure(ARC_RESOURCE_LIMIT, "The text chunk exceeds the output limit"); - } - if (const arc_status_t prepared = arc::abi::prepare_buffer_output(text_geometry, json.size()); - prepared != ARC_OK) { - return prepared; - } - std::memcpy(text_geometry->data, json.data(), json.size()); - return ARC_OK; - }); -} - -arc_status_t ARC_ABI_CALL arc_pdf_close(arc_handle_t document) -{ - return guarded([&]() -> arc_status_t { return table().close(document, document_kind); }); -} diff --git a/native/arcpdf-abi/tests/arcpdf_abi_engine_tests.cpp b/native/arcpdf-abi/tests/arcpdf_abi_engine_tests.cpp deleted file mode 100644 index 250ec071..00000000 --- a/native/arcpdf-abi/tests/arcpdf_abi_engine_tests.cpp +++ /dev/null @@ -1,969 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// Engine tests: the owned arc_pdf_* surface against a scripted fake backend (see fake_pdf_backend.hpp). They prove the -// limits, validation, handle lifetime, cancellation, deadline and output-encoding code. They prove nothing about -// PDFium. -#include - -#include "fake_pdf_backend.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace arc::pdf; -using namespace arc::pdf::testing; - -namespace { - -std::shared_ptr g_script; -std::shared_ptr g_backend; -int g_failures = 0; - -#define CHECK(expression) \ - do { \ - if (!(expression)) { \ - std::fprintf(stderr, "FAILED %s:%d: %s\n", __FILE__, __LINE__, #expression); \ - ++g_failures; \ - } \ - } while (false) - -#define CHECK_STATUS(expected, expression) \ - do { \ - const arc_status_t actual_status = (expression); \ - if (actual_status != (expected)) { \ - std::fprintf(stderr, "FAILED %s:%d: %s returned %d, expected %d\n", __FILE__, __LINE__, #expression, \ - static_cast(actual_status), static_cast(expected)); \ - ++g_failures; \ - } \ - } while (false) - -struct input_bytes final { - std::string bytes; - uint64_t short_reads = 0; // when nonzero, every read returns at most this many bytes - arc_status_t read_status = ARC_OK; - bool overrun = false; - bool zero_reads = false; -}; - -arc_status_t ARC_ABI_CALL read_input(void* context, uint64_t offset, void* destination, uint64_t requested, - uint64_t* done) -{ - auto* input = static_cast(context); - if (input->read_status != ARC_OK) { - return input->read_status; - } - if (input->zero_reads) { - *done = 0; - return ARC_OK; - } - if (input->overrun) { - *done = requested + 1; - return ARC_OK; - } - uint64_t count = std::min(requested, input->bytes.size() - offset); - if (input->short_reads != 0) { - count = std::min(count, input->short_reads); - } - std::memcpy(destination, input->bytes.data() + offset, static_cast(count)); - *done = count; - return ARC_OK; -} - -arc_io_v1 io_for(input_bytes& input) -{ - arc_io_v1 io{}; - io.struct_size = sizeof(arc_io_v1); - io.struct_version = 1; - io.context = &input; - io.length = input.bytes.size(); - io.max_length = input.bytes.size(); - io.read_at = read_input; - return io; -} - -arc_limits_v1 limits_for(uint32_t timeout_ms = 5000, uint32_t max_items = 1024) -{ - arc_limits_v1 limits{}; - limits.struct_size = sizeof(arc_limits_v1); - limits.struct_version = 1; - limits.max_input_bytes = 64ULL * 1024 * 1024; - limits.max_memory_bytes = 512ULL * 1024 * 1024; - limits.max_output_bytes = 3ULL * 64 * 1024 * 1024; - limits.max_width = 16384; - limits.max_height = 16384; - limits.max_items = max_items; - limits.timeout_ms = timeout_ms; - return limits; -} - -arc_string_view_t view(const char* text) -{ - return arc_string_view_t{text, std::strlen(text)}; -} - -fake_page page_with(const std::u16string& text, std::vector boxes = {}, - page_geometry geometry = {0, 612.0, 792.0}) -{ - fake_page page; - page.geometry = geometry; - page.content.text = text; - page.content.boxes = std::move(boxes); - return page; -} - -void reset(std::vector pages) -{ - g_script = std::make_shared(); - g_script->pages = std::move(pages); - g_backend = std::make_shared(g_script); -} - -struct opened final { - arc_handle_t handle = 0; - uint32_t pages = 0; -}; - -arc_status_t open_document(input_bytes& input, opened& out, const arc_limits_v1& limits = limits_for(), - arc_string_view_t password = {}, const arc_cancel_token_t* cancel = nullptr) -{ - const arc_io_v1 io = io_for(input); - return arc_pdf_open(&io, password, &limits, &out.handle, &out.pages, cancel); -} - -opened open_ok(const arc_limits_v1& limits = limits_for()) -{ - input_bytes input{"FAKEPDF1\nbody"}; - opened document; - CHECK_STATUS(ARC_OK, open_document(input, document, limits)); - return document; -} - -std::string fetch_text(arc_handle_t document, uint32_t page, uint32_t start, uint32_t count, - arc_status_t* status_out = nullptr) -{ - arc_mut_buffer_t query{}; - arc_status_t status = arc_pdf_text(document, page, start, count, &query, nullptr); - if (status != ARC_BUFFER_TOO_SMALL) { - if (status_out != nullptr) { - *status_out = status; - } - return {}; - } - std::string buffer(static_cast(query.required), '\0'); - arc_mut_buffer_t output{buffer.data(), buffer.size(), 0}; - status = arc_pdf_text(document, page, start, count, &output, nullptr); - if (status_out != nullptr) { - *status_out = status; - } - // The handle may legitimately be closed by another thread between the size query and this call (ARC_CLOSED). - if (status == ARC_OK) { - CHECK(output.required == buffer.size()); - } - return status == ARC_OK ? buffer : std::string{}; -} - -arc_pdf_page_v1 page_record() -{ - arc_pdf_page_v1 page{}; - page.struct_size = sizeof(arc_pdf_page_v1); - page.struct_version = 1; - return page; -} - -arc_region_v1 region_of(uint32_t x, uint32_t y, uint32_t width, uint32_t height, uint64_t stride = 0) -{ - arc_region_v1 region{}; - region.struct_size = sizeof(arc_region_v1); - region.struct_version = 1; - region.x = x; - region.y = y; - region.width = width; - region.height = height; - region.row_stride = stride; - return region; -} - -struct counting_cancel final { - std::atomic polls{0}; - int cancel_after = 0; // cancelled once polls exceeds this -}; - -arc_bool_t ARC_ABI_CALL cancel_after_polls(void* user) -{ - auto* state = static_cast(user); - return ++state->polls > state->cancel_after ? 1 : 0; -} - -arc_cancel_token_t token_for(counting_cancel& state) -{ - arc_cancel_token_t token{}; - token.struct_size = sizeof(arc_cancel_token_t); - token.struct_version = 1; - token.is_cancelled = cancel_after_polls; - token.user_data = &state; - return token; -} - -// The same 12-character tail the engine uses for the surrogate cases. -const std::u16string pair = u"\xD83D\xDE00"; // U+1F600 - -void test_preamble() -{ - reset({}); - uint32_t major = 0; - uint32_t minor = 0; - CHECK_STATUS(ARC_OK, arc_pdf_get_abi_version(&major, &minor)); - CHECK(major == 1 && minor == 1); - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_get_abi_version(nullptr, &minor)); - arc_mut_buffer_t query{}; - CHECK_STATUS(ARC_BUFFER_TOO_SMALL, arc_pdf_get_build_info(&query)); - std::string info(static_cast(query.required), '\0'); - arc_mut_buffer_t output{info.data(), info.size(), 0}; - CHECK_STATUS(ARC_OK, arc_pdf_get_build_info(&output)); - CHECK(info.rfind("ArcPdfNative;abi=1.1;backend=linked", 0) == 0); - arc_error_info_t error{}; - error.struct_size = sizeof(arc_error_info_t); - error.struct_version = 1; - CHECK_STATUS(ARC_BUFFER_TOO_SMALL, arc_pdf_get_last_error(&error)); - CHECK(error.status == ARC_INVALID_ARGUMENT); -} - -void test_open_validation() -{ - reset({page_with(u"a")}); - input_bytes input{"FAKEPDF1\nbody"}; - const arc_limits_v1 limits = limits_for(); - arc_io_v1 io = io_for(input); - opened out; - - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_open(nullptr, {}, &limits, &out.handle, &out.pages, nullptr)); - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_open(&io, {}, nullptr, &out.handle, &out.pages, nullptr)); - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_open(&io, {}, &limits, nullptr, &out.pages, nullptr)); - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_open(&io, {}, &limits, &out.handle, nullptr, nullptr)); - - arc_io_v1 bad = io; - bad.struct_size = sizeof(arc_io_v1) - 1; - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_open(&bad, {}, &limits, &out.handle, &out.pages, nullptr)); - bad = io; - bad.struct_version = 2; - CHECK_STATUS(ARC_VERSION_MISMATCH, arc_pdf_open(&bad, {}, &limits, &out.handle, &out.pages, nullptr)); - bad = io; - bad.read_at = nullptr; - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_open(&bad, {}, &limits, &out.handle, &out.pages, nullptr)); - bad = io; - bad.length = 0; - CHECK_STATUS(ARC_CORRUPT, arc_pdf_open(&bad, {}, &limits, &out.handle, &out.pages, nullptr)); - bad = io; - bad.length = 100; - bad.max_length = 50; - CHECK_STATUS(ARC_RESOURCE_LIMIT, arc_pdf_open(&bad, {}, &limits, &out.handle, &out.pages, nullptr)); - CHECK(out.handle == 0 && out.pages == 0); - - arc_limits_v1 zero = limits; - zero.max_items = 0; - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_open(&io, {}, &zero, &out.handle, &out.pages, nullptr)); - zero = limits; - zero.timeout_ms = 0; - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_open(&io, {}, &zero, &out.handle, &out.pages, nullptr)); - arc_limits_v1 over = limits; - over.max_width = 65536; - CHECK_STATUS(ARC_RESOURCE_LIMIT, arc_pdf_open(&io, {}, &over, &out.handle, &out.pages, nullptr)); - over = limits; - over.timeout_ms = 30001; - CHECK_STATUS(ARC_RESOURCE_LIMIT, arc_pdf_open(&io, {}, &over, &out.handle, &out.pages, nullptr)); - over = limits; - over.max_input_bytes = 256ULL * 1024 * 1024 + 1; - CHECK_STATUS(ARC_RESOURCE_LIMIT, arc_pdf_open(&io, {}, &over, &out.handle, &out.pages, nullptr)); - over = limits; - over.max_input_bytes = 4; // smaller than the input - CHECK_STATUS(ARC_RESOURCE_LIMIT, arc_pdf_open(&io, {}, &over, &out.handle, &out.pages, nullptr)); - - CHECK_STATUS(ARC_INVALID_ARGUMENT, - arc_pdf_open(&io, arc_string_view_t{nullptr, 3}, &limits, &out.handle, &out.pages, nullptr)); - const std::string long_password(1025, 'p'); - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_open(&io, arc_string_view_t{long_password.data(), long_password.size()}, - &limits, &out.handle, &out.pages, nullptr)); - CHECK(out.handle == 0 && out.pages == 0); - CHECK(g_script->alive_documents == 0); -} - -void test_open_backend_behaviour() -{ - input_bytes input{"FAKEPDF1\nbody"}; - opened out; - - reset({page_with(u"a")}); - input_bytes wrong{"%PDF-1.7 but not the fake"}; - CHECK_STATUS(ARC_CORRUPT, open_document(wrong, out)); - - reset({page_with(u"a")}); - g_script->required_password = "s3cret"; - CHECK_STATUS(ARC_PERMISSION_DENIED, open_document(input, out)); - CHECK_STATUS(ARC_PERMISSION_DENIED, open_document(input, out, limits_for(), view("nope"))); - CHECK_STATUS(ARC_OK, open_document(input, out, limits_for(), view("s3cret"))); - CHECK(out.pages == 1 && out.handle != 0); - CHECK_STATUS(ARC_OK, arc_pdf_close(out.handle)); - - reset({page_with(u"a")}); - g_script->throw_bad_alloc_on_open = true; - CHECK_STATUS(ARC_OUT_OF_MEMORY, open_document(input, out)); - reset({page_with(u"a")}); - g_script->throw_on_open = true; - CHECK_STATUS(ARC_INTERNAL, open_document(input, out)); - reset({page_with(u"a")}); - g_script->open_status = ARC_BUFFER_TOO_SMALL; // a positive code is not in a backend's closed set - CHECK_STATUS(ARC_INTERNAL, open_document(input, out)); - reset({page_with(u"a")}); - g_script->open_status = static_cast(-99); - CHECK_STATUS(ARC_INTERNAL, open_document(input, out)); - reset({page_with(u"a")}); - g_script->open_status = ARC_RESOURCE_LIMIT; - CHECK_STATUS(ARC_RESOURCE_LIMIT, open_document(input, out)); - reset({page_with(u"a")}); - g_script->null_document = true; - CHECK_STATUS(ARC_INTERNAL, open_document(input, out)); - reset({}); - CHECK_STATUS(ARC_CORRUPT, open_document(input, out)); // zero pages - reset({page_with(u"a")}); - g_script->reported_pages = (1U << 24) + 1; - CHECK_STATUS(ARC_RESOURCE_LIMIT, open_document(input, out)); - CHECK(out.handle == 0 && out.pages == 0); - CHECK(g_script->alive_documents == 0); -} - -void test_io_callbacks() -{ - reset({page_with(u"a")}); - opened out; - // A callback that returns the whole window is read in small steps by the backend. - input_bytes whole{std::string("FAKEPDF1\n") + std::string(3000, 'x')}; - CHECK_STATUS(ARC_OK, open_document(whole, out)); - CHECK(g_script->consumed_bytes == whole.bytes.size()); - CHECK_STATUS(ARC_OK, arc_pdf_close(out.handle)); - - // A short read in the middle of the input, or a zero-length OK read, violates the callback contract. - input_bytes shorts{std::string("FAKEPDF1\n") + std::string(3000, 'x'), 7}; - CHECK_STATUS(ARC_IO, open_document(shorts, out)); - input_bytes zero{"FAKEPDF1\nbody"}; - zero.zero_reads = true; - CHECK_STATUS(ARC_IO, open_document(zero, out)); - - input_bytes failing{"FAKEPDF1\nbody"}; - failing.read_status = ARC_IO; - CHECK_STATUS(ARC_IO, open_document(failing, out)); - input_bytes cancelled{"FAKEPDF1\nbody"}; - cancelled.read_status = ARC_CANCELLED; - CHECK_STATUS(ARC_CANCELLED, open_document(cancelled, out)); - input_bytes overrun{"FAKEPDF1\nbody"}; - overrun.overrun = true; - CHECK_STATUS(ARC_IO, open_document(overrun, out)); // done > requested is a defect of the caller's callback - CHECK(g_script->alive_documents == 0); -} - -void test_handle_lifetime() -{ - reset({page_with(u"a")}); - opened first = open_ok(); - CHECK(first.handle != 0); - arc_pdf_page_v1 page = page_record(); - CHECK_STATUS(ARC_OK, arc_pdf_page_info(first.handle, 0, &page)); - CHECK_STATUS(ARC_OK, arc_pdf_close(first.handle)); - CHECK(g_script->alive_documents == 0); - CHECK_STATUS(ARC_CLOSED, arc_pdf_close(first.handle)); - page = page_record(); - CHECK_STATUS(ARC_CLOSED, arc_pdf_page_info(first.handle, 0, &page)); - arc_mut_buffer_t out{}; - CHECK_STATUS(ARC_CLOSED, arc_pdf_text(first.handle, 0, 0, 10, &out, nullptr)); - CHECK_STATUS(ARC_CLOSED, arc_pdf_close(0)); - CHECK_STATUS(ARC_CLOSED, arc_pdf_close(UINT64_C(0xFFFFFFFFFFFFFFFF))); - - // The 64-document library limit, and no growth over repeated open/close. - std::vector held; - for (int i = 0; i < 64; ++i) { - held.push_back(open_ok()); - CHECK(held.back().handle != 0); - } - input_bytes input{"FAKEPDF1\nbody"}; - opened refused; - CHECK_STATUS(ARC_RESOURCE_LIMIT, open_document(input, refused)); - CHECK(refused.handle == 0 && g_script->alive_documents == 64); - for (const opened& document : held) { - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - } - CHECK(g_script->alive_documents == 0); - for (int i = 0; i < 1000; ++i) { - const opened document = open_ok(); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - } - CHECK(g_script->alive_documents == 0); -} - -void test_page_info() -{ - reset({page_with(u"a", {}, {0, 612.0, 792.0}), page_with(u"b", {}, {90, 300.5, 400.25})}); - const opened document = open_ok(); - CHECK(document.pages == 2); - arc_pdf_page_v1 page = page_record(); - CHECK_STATUS(ARC_OK, arc_pdf_page_info(document.handle, 1, &page)); - CHECK(page.page_index == 1 && page.rotation == 90 && page.width_points == 300.5 && page.height_points == 400.25); - page = page_record(); - CHECK_STATUS(ARC_NOT_FOUND, arc_pdf_page_info(document.handle, 2, &page)); - CHECK(page.width_points == 0 && page.height_points == 0); - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_page_info(document.handle, 0, nullptr)); - arc_pdf_page_v1 small = page_record(); - small.struct_size = 16; - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_page_info(document.handle, 0, &small)); - arc_pdf_page_v1 future = page_record(); - future.struct_version = 2; - CHECK_STATUS(ARC_VERSION_MISMATCH, arc_pdf_page_info(document.handle, 0, &future)); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - - const auto hostile = [](page_geometry geometry) { - reset({page_with(u"a", {}, geometry)}); - const opened doc = open_ok(); - arc_pdf_page_v1 record = page_record(); - const arc_status_t status = arc_pdf_page_info(doc.handle, 0, &record); - CHECK(record.width_points == 0 && record.height_points == 0 && record.rotation == 0); - CHECK_STATUS(ARC_OK, arc_pdf_close(doc.handle)); - return status; - }; - CHECK_STATUS(ARC_CORRUPT, hostile({0, std::nan(""), 792.0})); - CHECK_STATUS(ARC_CORRUPT, hostile({0, 612.0, std::numeric_limits::infinity()})); - CHECK_STATUS(ARC_CORRUPT, hostile({0, 0.0, 792.0})); - CHECK_STATUS(ARC_CORRUPT, hostile({0, -5.0, 792.0})); - CHECK_STATUS(ARC_CORRUPT, hostile({0, 2000000.0, 792.0})); - CHECK_STATUS(ARC_CORRUPT, hostile({45, 612.0, 792.0})); - CHECK_STATUS(ARC_CORRUPT, hostile({360, 612.0, 792.0})); - - reset({page_with(u"a")}); - g_script->geometry_status = ARC_CORRUPT; - const opened failing = open_ok(); - arc_pdf_page_v1 record = page_record(); - CHECK_STATUS(ARC_CORRUPT, arc_pdf_page_info(failing.handle, 0, &record)); - CHECK_STATUS(ARC_OK, arc_pdf_close(failing.handle)); -} - -void test_text_basic_and_paging() -{ - reset({page_with(u"hello world", {{0, 5, 1, 2, 3, 4}, {6, 5, 10.5, 2, 30, 4}})}); - const opened document = open_ok(); - arc_status_t status = ARC_OK; - std::string json = fetch_text(document.handle, 0, 0, 65536, &status); - CHECK_STATUS(ARC_OK, status); - CHECK(json == "{\"version\":1,\"page\":0,\"start\":0,\"text\":\"hello world\",\"boxes\":[" - "{\"start\":0,\"length\":5,\"x\":1,\"y\":2,\"width\":3,\"height\":4}," - "{\"start\":6,\"length\":5,\"x\":10.5,\"y\":2,\"width\":30,\"height\":4}]}"); - // Chunk of five: only the box wholly inside; next points at the following position. - json = fetch_text(document.handle, 0, 0, 5, &status); - CHECK_STATUS(ARC_OK, status); - CHECK(json == "{\"version\":1,\"page\":0,\"start\":0,\"next\":5,\"text\":\"hello\",\"boxes\":[" - "{\"start\":0,\"length\":5,\"x\":1,\"y\":2,\"width\":3,\"height\":4}]}"); - json = fetch_text(document.handle, 0, 5, 3, - &status); // the box at 6..11 would straddle the window end, so the chunk ends where it starts - CHECK(json == "{\"version\":1,\"page\":0,\"start\":5,\"next\":6,\"text\":\" \",\"boxes\":[]}"); - json = fetch_text(document.handle, 0, 8, 100, &status); - CHECK(json == "{\"version\":1,\"page\":0,\"start\":8,\"text\":\"rld\",\"boxes\":[]}"); - json = fetch_text(document.handle, 0, 11, 10, &status); // at the end: empty chunk, no next - CHECK_STATUS(ARC_OK, status); - CHECK(json == "{\"version\":1,\"page\":0,\"start\":11,\"text\":\"\",\"boxes\":[]}"); - - CHECK_STATUS(ARC_INVALID_ARGUMENT, [&] { - fetch_text(document.handle, 0, 12, 10, &status); - return status; - }()); - CHECK_STATUS(ARC_INVALID_ARGUMENT, [&] { - fetch_text(document.handle, 0, 0, 0, &status); - return status; - }()); - CHECK_STATUS(ARC_INVALID_ARGUMENT, [&] { - fetch_text(document.handle, 0, 0, 65537, &status); - return status; - }()); - CHECK_STATUS(ARC_NOT_FOUND, [&] { - fetch_text(document.handle, 1, 0, 10, &status); - return status; - }()); - - // Too-small buffer reports the exact size and writes nothing; the retry succeeds with the same size. - char small[8] = {}; - arc_mut_buffer_t tight{small, sizeof(small), 0}; - CHECK_STATUS(ARC_BUFFER_TOO_SMALL, arc_pdf_text(document.handle, 0, 0, 65536, &tight, nullptr)); - CHECK(tight.required > sizeof(small) && small[0] == '\0'); - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_text(document.handle, 0, 0, 10, nullptr, nullptr)); - arc_mut_buffer_t nulldata{nullptr, 5, 0}; - CHECK_STATUS(ARC_INVALID_ARGUMENT, arc_pdf_text(document.handle, 0, 0, 10, &nulldata, nullptr)); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); -} - -void test_text_surrogates_and_escaping() -{ - const std::u16string text = u"a" + pair + u"b" + pair; - reset({page_with(text, {{0, 1, 0, 0, 1, 1}, {1, 2, 1, 0, 2, 1}})}); - const opened document = open_ok(); - arc_status_t status = ARC_OK; - // A one-unit window that lands on a pair takes the whole pair so every chunk makes progress. - std::string json = fetch_text(document.handle, 0, 1, 1, &status); - CHECK_STATUS(ARC_OK, status); - CHECK(json.find("\"next\":3") != std::string::npos); - CHECK(json.find("\"text\":\"\xF0\x9F\x98\x80\"") != std::string::npos); - // A window that would end inside the pair backs up before it. - json = fetch_text(document.handle, 0, 0, 2, &status); - CHECK(json.find("\"next\":1") != std::string::npos && json.find("\"text\":\"a\"") != std::string::npos); - // A start inside the pair is refused. - CHECK_STATUS(ARC_INVALID_ARGUMENT, [&] { - fetch_text(document.handle, 0, 2, 5, &status); - return status; - }()); - // Walking the whole page by 2-unit windows reproduces the text exactly. - std::string rebuilt; - uint32_t start = 0; - for (int guard = 0; guard < 100; ++guard) { - json = fetch_text(document.handle, 0, start, 2, &status); - CHECK_STATUS(ARC_OK, status); - const size_t open = json.find("\"text\":\"") + 8; - const size_t close = json.find("\",\"boxes\""); - rebuilt += json.substr(open, close - open); - const size_t next = json.find("\"next\":"); - if (next == std::string::npos || next > json.find("\"text\"")) { - break; - } - start = static_cast(std::stoul(json.substr(next + 7))); - } - CHECK(rebuilt == "a\xF0\x9F\x98\x80" - "b\xF0\x9F\x98\x80"); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - - const std::u16string odd = - std::u16string(u"q\"\\\n") + char16_t(0x01) + char16_t(0xD800) + u"x" + char16_t(0xDC00) + char16_t(0x00E9); - reset({page_with(odd)}); - const opened escaped = open_ok(); - json = fetch_text(escaped.handle, 0, 0, 100, &status); - CHECK_STATUS(ARC_OK, status); - CHECK(json == "{\"version\":1,\"page\":0,\"start\":0,\"text\":\"q\\\"\\\\\\u000a\\u0001\xEF\xBF\xBD" - "x\xEF\xBF\xBD\xC3\xA9\",\"boxes\":[]}"); - CHECK_STATUS(ARC_OK, arc_pdf_close(escaped.handle)); -} - -void test_text_limits_and_hostile_backend() -{ - // Box budget: with max_items 2 a chunk of five boxes is cut before the third box. - reset({page_with( - u"aa bb cc dd ee", - {{0, 2, 0, 0, 1, 1}, {3, 2, 0, 0, 1, 1}, {6, 2, 0, 0, 1, 1}, {9, 2, 0, 0, 1, 1}, {12, 2, 0, 0, 1, 1}})}); - opened document = open_ok(limits_for(5000, 2)); - arc_status_t status = ARC_OK; - std::string json = fetch_text(document.handle, 0, 0, 65536, &status); - CHECK_STATUS(ARC_OK, status); - CHECK(json.find("\"next\":6") != std::string::npos); - CHECK(json.find("\"start\":3,\"length\":2") != std::string::npos && - json.find("\"start\":6,\"length\":2") == std::string::npos); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - - // Many boxes at the very start of a window still make progress. - reset({page_with(u"abcd", {{0, 1, 0, 0, 1, 1}, {0, 1, 0, 0, 1, 1}, {0, 2, 0, 0, 1, 1}})}); - document = open_ok(limits_for(5000, 2)); - json = fetch_text(document.handle, 0, 0, 4, &status); - CHECK_STATUS(ARC_OK, - status); // progress by one unit; a box that crosses the chunk end is not carried - // (documented pager rule) - CHECK(json.find("\"next\":1") != std::string::npos); - CHECK(json.find("\"length\":2") == std::string::npos); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - - const auto hostile = [](std::u16string text, std::vector boxes) { - reset({page_with(text, std::move(boxes))}); - const opened doc = open_ok(); - arc_status_t status = ARC_OK; - const std::string json = fetch_text(doc.handle, 0, 0, 100, &status); - CHECK(json.empty()); - CHECK_STATUS(ARC_OK, arc_pdf_close(doc.handle)); - return status; - }; - CHECK_STATUS(ARC_CORRUPT, hostile(u"abc", {{2, 1, 0, 0, 1, 1}, {0, 1, 0, 0, 1, 1}})); // unordered - CHECK_STATUS(ARC_CORRUPT, hostile(u"abc", {{2, 5, 0, 0, 1, 1}})); // past the text - CHECK_STATUS(ARC_CORRUPT, hostile(u"abc", {{0, 0, 0, 0, 1, 1}})); // empty box - CHECK_STATUS(ARC_CORRUPT, hostile(u"abc", {{0, 1, std::nan(""), 0, 1, 1}})); // not finite - CHECK_STATUS(ARC_CORRUPT, hostile(u"abc", {{0, 1, 0, 0, -1, 1}})); // negative extent - CHECK_STATUS(ARC_CORRUPT, hostile(u"abc", {{0, 1, 0, 0, std::numeric_limits::infinity(), 1}})); - CHECK_STATUS(ARC_RESOURCE_LIMIT, hostile(std::u16string((16U * 1024 * 1024) + 1, u'x'), {})); - - reset({page_with(u"abc")}); - g_script->text_status = ARC_CORRUPT; - document = open_ok(); - CHECK_STATUS(ARC_CORRUPT, [&] { - fetch_text(document.handle, 0, 0, 10, &status); - return status; - }()); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - - // The page text is fetched once and reused across chunks of the same page. - reset({page_with(u"abcdef"), page_with(u"ghi")}); - document = open_ok(); - for (uint32_t start = 0; start < 6; ++start) { - fetch_text(document.handle, 0, start, 1, &status); - CHECK_STATUS(ARC_OK, status); - } - CHECK(g_script->text_calls == 1); - fetch_text(document.handle, 1, 0, 1, &status); - CHECK(g_script->text_calls == 2); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); -} - -void test_render() -{ - reset({page_with(u"a"), page_with(u"b", {}, {90, 300.0, 400.0})}); - const opened document = open_ok(); - arc_pdf_page_v1 page = page_record(); - CHECK_STATUS(ARC_OK, arc_pdf_page_info(document.handle, 1, &page)); - - // 4x2 tile at (10, 20) of a 100x200 grid, packed. - const arc_region_v1 tile = region_of(10, 20, 4, 2); - std::vector pixels(4 * 4 * 2, 0xEE); - arc_mut_buffer_t buffer{pixels.data(), pixels.size(), 0}; - CHECK_STATUS(ARC_OK, arc_pdf_render(document.handle, &page, &tile, 100, 200, &buffer, nullptr)); - CHECK(buffer.required == 32); - CHECK(pixels[0] == 10 && pixels[1] == 20 && pixels[2] == 1 && pixels[3] == 255); - CHECK(pixels[4 * 4 + 0] == 10 && pixels[4 * 4 + 1] == 21); - CHECK(pixels[(4 * 4 + 3 * 4) + 0] == 13); - - // A stride wider than the row: required covers (height - 1) strides plus one row, and no byte past it - // is touched. - const arc_region_v1 strided = region_of(0, 0, 3, 2, 20); - std::vector big(64, 0xEE); - arc_mut_buffer_t strided_buffer{big.data(), big.size(), 0}; - CHECK_STATUS(ARC_OK, arc_pdf_render(document.handle, &page, &strided, 100, 200, &strided_buffer, nullptr)); - CHECK(strided_buffer.required == 20 + 12); - CHECK(big[31] == 255 && big[32] == 0xEE && big[63] == 0xEE); - CHECK(big[12] == 0xEE); // the gap between rows is not written - - // Too small: exact required size reported, nothing written. - std::vector tiny(8, 0xEE); - arc_mut_buffer_t tiny_buffer{tiny.data(), tiny.size(), 0}; - CHECK_STATUS(ARC_BUFFER_TOO_SMALL, arc_pdf_render(document.handle, &page, &tile, 100, 200, &tiny_buffer, nullptr)); - CHECK(tiny_buffer.required == 32 && tiny[0] == 0xEE); - - // Validation. - const auto render = [&](const arc_region_v1& region, uint32_t width, uint32_t height, - const arc_pdf_page_v1* record = nullptr) { - std::vector out(1 << 20, 0xEE); - arc_mut_buffer_t b{out.data(), out.size(), 0}; - const arc_status_t status = - arc_pdf_render(document.handle, record == nullptr ? &page : record, ®ion, width, height, &b, nullptr); - if (status != ARC_OK) { - CHECK(std::all_of(out.begin(), out.end(), [](uint8_t v) { return v == 0xEE || v == 0; })); - } - return status; - }; - CHECK_STATUS(ARC_INVALID_ARGUMENT, render(region_of(0, 0, 0, 4), 100, 100)); - CHECK_STATUS(ARC_INVALID_ARGUMENT, render(region_of(0, 0, 4, 0), 100, 100)); - CHECK_STATUS(ARC_INVALID_ARGUMENT, render(region_of(0, 0, 4, 4), 0, 100)); - CHECK_STATUS(ARC_INVALID_ARGUMENT, render(region_of(98, 0, 4, 4), 100, 100)); - CHECK_STATUS(ARC_INVALID_ARGUMENT, render(region_of(0, 98, 4, 4), 100, 100)); - CHECK_STATUS(ARC_INVALID_ARGUMENT, render(region_of(UINT32_MAX, 0, 4, 4), 100, 100)); - CHECK_STATUS(ARC_INVALID_ARGUMENT, render(region_of(0, 0, 4, 4, 15), 100, 100)); - arc_region_v1 samples = region_of(0, 0, 4, 4); - samples.first_sample = 1; - CHECK_STATUS(ARC_INVALID_ARGUMENT, render(samples, 100, 100)); - arc_region_v1 bad_record = region_of(0, 0, 4, 4); - bad_record.struct_version = 3; - CHECK_STATUS(ARC_VERSION_MISMATCH, render(bad_record, 100, 100)); - CHECK_STATUS(ARC_RESOURCE_LIMIT, render(region_of(0, 0, 4, 4), 16385, 100)); - CHECK_STATUS(ARC_RESOURCE_LIMIT, render(region_of(0, 0, 4, 4), 100, 16385)); - arc_pdf_page_v1 scaled = page; - scaled.width_points *= 2; - CHECK_STATUS(ARC_INVALID_ARGUMENT, render(region_of(0, 0, 4, 4), 100, 100, &scaled)); - arc_pdf_page_v1 rotated = page; - rotated.rotation = 0; - CHECK_STATUS(ARC_INVALID_ARGUMENT, render(region_of(0, 0, 4, 4), 100, 100, &rotated)); - arc_pdf_page_v1 other = page; - other.page_index = 7; - CHECK_STATUS(ARC_NOT_FOUND, render(region_of(0, 0, 4, 4), 100, 100, &other)); - CHECK_STATUS(ARC_INVALID_ARGUMENT, [&] { - arc_mut_buffer_t none{}; - return arc_pdf_render(document.handle, nullptr, &tile, 100, 200, &none, nullptr); - }()); - CHECK_STATUS(ARC_INVALID_ARGUMENT, - [&] { return arc_pdf_render(document.handle, &page, &tile, 100, 200, nullptr, nullptr); }()); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - - // A grid beyond the pixel bound even though each side is within the dimension limit. - arc_limits_v1 wide = limits_for(); - wide.max_width = 65535; - wide.max_height = 65535; - reset({page_with(u"a")}); - const opened huge = open_ok(wide); - arc_pdf_page_v1 huge_page = page_record(); - CHECK_STATUS(ARC_OK, arc_pdf_page_info(huge.handle, 0, &huge_page)); - std::vector small_out(64); - arc_mut_buffer_t small_buffer{small_out.data(), small_out.size(), 0}; - const arc_region_v1 corner = region_of(0, 0, 2, 2); - CHECK_STATUS(ARC_RESOURCE_LIMIT, - arc_pdf_render(huge.handle, &huge_page, &corner, 20000, 20000, &small_buffer, nullptr)); - CHECK(g_script->render_calls == 0); - CHECK_STATUS(ARC_OK, arc_pdf_close(huge.handle)); - - // The output limit: a tile larger than max_output_bytes is refused before the backend is called. - arc_limits_v1 narrow = limits_for(); - narrow.max_output_bytes = 16; - reset({page_with(u"a")}); - const opened bounded = open_ok(narrow); - arc_pdf_page_v1 bounded_page = page_record(); - CHECK_STATUS(ARC_OK, arc_pdf_page_info(bounded.handle, 0, &bounded_page)); - std::vector room(64); - arc_mut_buffer_t room_buffer{room.data(), room.size(), 0}; - const arc_region_v1 five = region_of(0, 0, 5, 1); - CHECK_STATUS(ARC_RESOURCE_LIMIT, - arc_pdf_render(bounded.handle, &bounded_page, &five, 100, 100, &room_buffer, nullptr)); - CHECK(g_script->render_calls == 0); - CHECK_STATUS(ARC_OK, arc_pdf_close(bounded.handle)); -} - -void test_render_failures_leave_no_partial_output() -{ - reset({page_with(u"a")}); - g_script->render_fails_after_writing = true; - opened document = open_ok(); - arc_pdf_page_v1 page = page_record(); - CHECK_STATUS(ARC_OK, arc_pdf_page_info(document.handle, 0, &page)); - const arc_region_v1 tile = region_of(0, 0, 8, 8); - std::vector out(8 * 8 * 4 + 16, 0xEE); - arc_mut_buffer_t buffer{out.data(), out.size(), 0}; - CHECK_STATUS(ARC_CORRUPT, arc_pdf_render(document.handle, &page, &tile, 64, 64, &buffer, nullptr)); - CHECK(g_script->render_calls == 1); - CHECK(std::all_of(out.begin(), out.begin() + 8 * 8 * 4, [](uint8_t v) { return v == 0; })); // pixels wiped - CHECK(out[8 * 8 * 4] == 0xEE); // nothing past the tile touched - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - - reset({page_with(u"a")}); - g_script->throw_on_render = true; - document = open_ok(); - page = page_record(); - CHECK_STATUS(ARC_OK, arc_pdf_page_info(document.handle, 0, &page)); - out.assign(out.size(), 0xEE); - buffer = {out.data(), out.size(), 0}; - CHECK_STATUS(ARC_INTERNAL, arc_pdf_render(document.handle, &page, &tile, 64, 64, &buffer, nullptr)); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); -} - -void test_cancellation_and_deadline() -{ - // Cancelled before the call: the backend is never reached. - reset({page_with(u"a")}); - counting_cancel now_cancelled{0, -1}; - arc_cancel_token_t token = token_for(now_cancelled); - input_bytes input{"FAKEPDF1\nbody"}; - opened out; - CHECK_STATUS(ARC_CANCELLED, open_document(input, out, limits_for(), {}, &token)); - CHECK(g_script->alive_documents == 0 && out.handle == 0); - - arc_cancel_token_t invalid = token; - invalid.struct_size = 8; - CHECK_STATUS(ARC_INVALID_ARGUMENT, open_document(input, out, limits_for(), {}, &invalid)); - arc_cancel_token_t no_callback = token; - no_callback.is_cancelled = nullptr; - CHECK_STATUS(ARC_INVALID_ARGUMENT, open_document(input, out, limits_for(), {}, &no_callback)); - - // A parser that spins is stopped by a cancellation that arrives mid-call. - reset({page_with(u"a")}); - g_script->open_hangs_until_stopped = true; - counting_cancel later{0, 20}; - token = token_for(later); - CHECK_STATUS(ARC_CANCELLED, open_document(input, out, limits_for(), {}, &token)); - CHECK(later.polls > 20 && g_script->alive_documents == 0); - - // The deadline stops a spinning parser with no cancellation at all. - reset({page_with(u"a")}); - g_script->open_hangs_until_stopped = true; - const auto before = std::chrono::steady_clock::now(); - CHECK_STATUS(ARC_RESOURCE_LIMIT, open_document(input, out, limits_for(100))); - const auto elapsed = std::chrono::steady_clock::now() - before; - CHECK(elapsed >= std::chrono::milliseconds(90) && elapsed < std::chrono::seconds(10)); - - // A render that spins is cancelled and its buffer is wiped; a render that ignores the deadline is - // refused afterwards. - reset({page_with(u"a")}); - g_script->render_hangs_until_stopped = true; - opened document = open_ok(); - arc_pdf_page_v1 page = page_record(); - CHECK_STATUS(ARC_OK, arc_pdf_page_info(document.handle, 0, &page)); - const arc_region_v1 tile = region_of(0, 0, 4, 4); - std::vector pixels(64, 0xEE); - arc_mut_buffer_t buffer{pixels.data(), pixels.size(), 0}; - counting_cancel render_cancel{0, 10}; - token = token_for(render_cancel); - CHECK_STATUS(ARC_CANCELLED, arc_pdf_render(document.handle, &page, &tile, 64, 64, &buffer, &token)); - CHECK(std::all_of(pixels.begin(), pixels.end(), [](uint8_t v) { return v == 0; })); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - - reset({page_with(u"a")}); - g_script->render_ignores_deadline_ms = true; - g_script->render_sleep_ms = 250; - document = open_ok(limits_for(60)); - page = page_record(); - CHECK_STATUS(ARC_OK, arc_pdf_page_info(document.handle, 0, &page)); - pixels.assign(64, 0xEE); - buffer = {pixels.data(), pixels.size(), 0}; - CHECK_STATUS(ARC_RESOURCE_LIMIT, arc_pdf_render(document.handle, &page, &tile, 64, 64, &buffer, nullptr)); - CHECK(std::all_of(pixels.begin(), pixels.end(), [](uint8_t v) { return v == 0; })); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - - // Text honours a token too. - reset({page_with(u"abc")}); - document = open_ok(); - counting_cancel text_cancel{0, -1}; - token = token_for(text_cancel); - arc_mut_buffer_t query{}; - CHECK_STATUS(ARC_CANCELLED, arc_pdf_text(document.handle, 0, 0, 10, &query, &token)); - CHECK(g_script->text_calls == 0); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); -} - -arc_bool_t ARC_ABI_CALL flag_cancel(void* user) -{ - return static_cast*>(user)->load() ? 1 : 0; -} - -void test_review_findings() -{ - // A box that would straddle a chunk end moves whole into the next chunk, so every box is delivered - // exactly once. - reset({page_with(u"0123456789", {{0, 4, 0, 0, 1, 1}, {4, 4, 0, 0, 1, 1}, {8, 2, 0, 0, 1, 1}})}); - opened document = open_ok(); - arc_status_t status = ARC_OK; - for (uint32_t count : {6U, 5U}) { - int delivered = 0; - uint32_t start = 0; - for (int guard = 0; guard < 20; ++guard) { - const std::string json = fetch_text(document.handle, 0, start, count, &status); - CHECK_STATUS(ARC_OK, status); - for (size_t at = json.find("\"length\""); at != std::string::npos; at = json.find("\"length\"", at + 1)) { - ++delivered; - } - const size_t next = json.find("\"next\":"); - if (next == std::string::npos || next > json.find("\"text\"")) { - break; - } - start = static_cast(std::stoul(json.substr(next + 7))); - } - CHECK(delivered == 3); - } - std::string json = fetch_text(document.handle, 0, 0, 6, &status); - CHECK(json.find("\"next\":4") != std::string::npos && json.find("\"text\":\"0123\"") != std::string::npos); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - - // The output limit bounds the text response. - reset({page_with(std::u16string(100, u'a'))}); - arc_limits_v1 small_output = limits_for(); - small_output.max_output_bytes = 100; - document = open_ok(small_output); - fetch_text(document.handle, 0, 0, 100, &status); - CHECK_STATUS(ARC_RESOURCE_LIMIT, status); - fetch_text(document.handle, 0, 0, 10, &status); // a shorter chunk fits under the same limit - CHECK_STATUS(ARC_OK, status); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - - // A row stride beyond the tile bound is refused even when the tile itself is tiny. - reset({page_with(u"a")}); - document = open_ok(); - arc_pdf_page_v1 page = page_record(); - CHECK_STATUS(ARC_OK, arc_pdf_page_info(document.handle, 0, &page)); - const arc_region_v1 wide_stride = region_of(0, 0, 4, 1, 64ULL * 1024 * 1024 + 1); - std::vector room(64, 0xEE); - arc_mut_buffer_t room_buffer{room.data(), room.size(), 0}; - CHECK_STATUS(ARC_INVALID_ARGUMENT, - arc_pdf_render(document.handle, &page, &wide_stride, 100, 100, &room_buffer, nullptr)); - CHECK(g_script->render_calls == 0 && room[0] == 0xEE); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); - - // A cancellation that arrives while the backend answers the geometry call is honoured before the - // render. - reset({page_with(u"a")}); - document = open_ok(); - std::atomic cancelled{false}; - g_script->on_geometry = [&] { cancelled = true; }; - arc_cancel_token_t token{}; - token.struct_size = sizeof(arc_cancel_token_t); - token.struct_version = 1; - token.is_cancelled = flag_cancel; - token.user_data = &cancelled; - page = page_record(); - page.page_index = 0; - page.rotation = 0; - page.width_points = 612.0; - page.height_points = 792.0; - const arc_region_v1 tile = region_of(0, 0, 4, 4); - std::vector pixels(64, 0xEE); - arc_mut_buffer_t pixel_buffer{pixels.data(), pixels.size(), 0}; - CHECK_STATUS(ARC_CANCELLED, arc_pdf_render(document.handle, &page, &tile, 100, 100, &pixel_buffer, &token)); - CHECK(g_script->geometry_calls == 1 && g_script->render_calls == 0); - CHECK_STATUS(ARC_OK, arc_pdf_close(document.handle)); -} - -void test_concurrent_use_and_close() -{ - reset({page_with(u"abcdefghij", {{0, 3, 0, 0, 1, 1}})}); - const opened document = open_ok(); - std::atomic successes{0}; - std::atomic closed{0}; - std::atomic unexpected{0}; - std::vector workers; - for (int t = 0; t < 8; ++t) { - workers.emplace_back([&] { - for (int i = 0; i < 300; ++i) { - arc_status_t status = ARC_OK; - const std::string json = fetch_text(document.handle, 0, static_cast(i % 5), 4, &status); - if (status == ARC_OK && !json.empty()) { - ++successes; - } else if (status == ARC_CLOSED) { - ++closed; - } else { - ++unexpected; - } - } - }); - } - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - const arc_status_t close_status = arc_pdf_close(document.handle); - for (std::thread& worker : workers) { - worker.join(); - } - CHECK_STATUS(ARC_OK, close_status); - CHECK(unexpected == 0 && successes + closed == 8 * 300 && successes > 0); - CHECK(g_script->alive_documents == 0); -} - -} // namespace - -std::shared_ptr arc::pdf::linked_backend() -{ - return g_backend; -} - -int main() -{ - test_preamble(); - test_open_validation(); - test_open_backend_behaviour(); - test_io_callbacks(); - test_handle_lifetime(); - test_page_info(); - test_text_basic_and_paging(); - test_text_surrogates_and_escaping(); - test_text_limits_and_hostile_backend(); - test_render(); - test_render_failures_leave_no_partial_output(); - test_cancellation_and_deadline(); - test_review_findings(); - test_concurrent_use_and_close(); - if (g_failures != 0) { - std::fprintf(stderr, "%d check(s) failed\n", g_failures); - return 1; - } - std::puts("arcpdf_abi_engine_tests: all checks passed"); - return 0; -} diff --git a/native/arcpdf-abi/tests/arcpdf_abi_unsupported_tests.cpp b/native/arcpdf-abi/tests/arcpdf_abi_unsupported_tests.cpp deleted file mode 100644 index 3b2a1224..00000000 --- a/native/arcpdf-abi/tests/arcpdf_abi_unsupported_tests.cpp +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// The shipped library configuration: no PDF parser is linked, so every open fails closed with ARC_UNSUPPORTED and -// leaves every output zeroed. This test links the real arcpdf_abi target, not the fake backend. -#include - -#include -#include -#include - -namespace { -int g_failures = 0; -#define CHECK(expression) \ - do { \ - if (!(expression)) { \ - std::fprintf(stderr, "FAILED %s:%d: %s\n", __FILE__, __LINE__, #expression); \ - ++g_failures; \ - } \ - } while (false) - -arc_status_t ARC_ABI_CALL never_read(void*, uint64_t, void*, uint64_t, uint64_t* done) -{ - *done = 0; - return ARC_IO; -} -} // namespace - -int main() -{ - uint32_t major = 0; - uint32_t minor = 0; - CHECK(arc_pdf_get_abi_version(&major, &minor) == ARC_OK && major == 1 && minor == 1); - - arc_mut_buffer_t query{}; - CHECK(arc_pdf_get_build_info(&query) == ARC_BUFFER_TOO_SMALL); - std::string info(static_cast(query.required), '\0'); - arc_mut_buffer_t output{info.data(), info.size(), 0}; - CHECK(arc_pdf_get_build_info(&output) == ARC_OK); - CHECK(info.find("backend=none") != std::string::npos); - - arc_io_v1 io{}; - io.struct_size = sizeof(arc_io_v1); - io.struct_version = 1; - io.length = 16; - io.max_length = 16; - io.read_at = never_read; - arc_limits_v1 limits{}; - limits.struct_size = sizeof(arc_limits_v1); - limits.struct_version = 1; - limits.max_input_bytes = 1024; - limits.max_memory_bytes = 1024 * 1024; - limits.max_output_bytes = 1024 * 1024; - limits.max_width = 1024; - limits.max_height = 1024; - limits.max_items = 16; - limits.timeout_ms = 1000; - arc_handle_t document = 99; - uint32_t pages = 99; - CHECK(arc_pdf_open(&io, {}, &limits, &document, &pages, nullptr) == ARC_UNSUPPORTED); - CHECK(document == 0 && pages == 0); - - arc_error_info_t error{}; - error.struct_size = sizeof(arc_error_info_t); - error.struct_version = 1; - CHECK(arc_pdf_get_last_error(&error) == ARC_BUFFER_TOO_SMALL && error.status == ARC_UNSUPPORTED); - - // Every other export refuses a handle the library never issued. - arc_pdf_page_v1 page{}; - page.struct_size = sizeof(arc_pdf_page_v1); - page.struct_version = 1; - CHECK(arc_pdf_page_info(1, 0, &page) == ARC_CLOSED); - CHECK(arc_pdf_close(1) == ARC_CLOSED); - - if (g_failures != 0) { - return 1; - } - std::puts("arcpdf_abi_unsupported_tests: all checks passed"); - return 0; -} diff --git a/native/arcpdf-abi/tests/fake_pdf_backend.hpp b/native/arcpdf-abi/tests/fake_pdf_backend.hpp deleted file mode 100644 index 7aa72ac4..00000000 --- a/native/arcpdf-abi/tests/fake_pdf_backend.hpp +++ /dev/null @@ -1,187 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// TEST ONLY. A scripted PDF backend that stands in for PDFium so the owned engine's limits, validation, handle, -// cancellation, deadline and output-encoding code is exercised against hostile backend behaviour. It parses no real -// PDF, is compiled only into the arcpdf test executables, and is never linked into the shipped library. -#ifndef ARC_PDF_FAKE_BACKEND_HPP -#define ARC_PDF_FAKE_BACKEND_HPP - -#include "pdf_backend.hpp" - -#include -#include -#include -#include -#include -#include - -namespace arc::pdf::testing { - -struct fake_page final { - page_geometry geometry{0, 612.0, 792.0}; - page_text content; -}; - -struct fake_script final { - std::vector pages; - std::string required_password; // empty: not encrypted - bool throw_on_open = false; - bool throw_bad_alloc_on_open = false; - bool throw_on_render = false; - bool null_document = false; - bool open_hangs_until_stopped = false; // loops on context.check() - bool render_hangs_until_stopped = false; - bool render_ignores_deadline_ms = false; // sleeps render_sleep_ms and returns OK - uint32_t render_sleep_ms = 0; - bool render_fails_after_writing = false; // writes the whole tile, then reports failure - arc_status_t open_status = ARC_OK; // returned instead of opening when not ARC_OK - arc_status_t geometry_status = ARC_OK; - arc_status_t text_status = ARC_OK; - uint32_t reported_pages = 0; // 0: pages.size() - std::function on_geometry; // runs inside every geometry call - uint64_t consumed_bytes = 0; // set by open: bytes read through the source - std::atomic alive_documents{0}; - std::atomic render_calls{0}; - std::atomic text_calls{0}; - std::atomic geometry_calls{0}; -}; - -inline arc_status_t wait_for_stop(const call_context& context) -{ - for (;;) { - const arc_status_t status = context.check(); - if (status != ARC_OK) { - return status; - } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } -} - -class fake_document final : public document { - public: - explicit fake_document(std::shared_ptr script) : script_(std::move(script)) - { - ++script_->alive_documents; - } - ~fake_document() override - { - --script_->alive_documents; - } - - arc_status_t geometry(uint32_t index, const call_context&, page_geometry& out) override - { - ++script_->geometry_calls; - if (script_->on_geometry) { - script_->on_geometry(); - } - if (script_->geometry_status != ARC_OK) { - return script_->geometry_status; - } - out = script_->pages.at(index).geometry; - return ARC_OK; - } - - arc_status_t text(uint32_t index, const call_context&, page_text& out) override - { - ++script_->text_calls; - if (script_->text_status != ARC_OK) { - return script_->text_status; - } - out = script_->pages.at(index).content; - return ARC_OK; - } - - // Pixel (column, row) of the full grid is {column & 255, row & 255, page & 255, 255}. - arc_status_t render(uint32_t index, uint32_t, uint32_t, const region& tile, uint64_t row_stride, uint8_t* out, - uint64_t out_size, const call_context& context) override - { - ++script_->render_calls; - if (script_->throw_on_render) { - throw std::runtime_error("render raised"); - } - if (script_->render_hangs_until_stopped) { - return wait_for_stop(context); - } - if (script_->render_ignores_deadline_ms) { - std::this_thread::sleep_for(std::chrono::milliseconds(script_->render_sleep_ms)); - } - for (uint32_t row = 0; row < tile.height; ++row) { - for (uint32_t column = 0; column < tile.width; ++column) { - const uint64_t offset = row * row_stride + static_cast(column) * 4; - if (offset + 4 > out_size) { - return ARC_INTERNAL; // the engine promised exactly the bytes up to the last pixel - } - out[offset + 0] = static_cast((tile.x + column) & 255U); - out[offset + 1] = static_cast((tile.y + row) & 255U); - out[offset + 2] = static_cast(index & 255U); - out[offset + 3] = 255; - } - } - return script_->render_fails_after_writing ? ARC_CORRUPT : ARC_OK; - } - - private: - std::shared_ptr script_; -}; - -class fake_backend final : public backend { - public: - explicit fake_backend(std::shared_ptr script) : script_(std::move(script)) - { - } - - arc_status_t open(std::shared_ptr source, std::string_view password, const call_context& context, - std::unique_ptr& out, uint32_t& pages) override - { - if (script_->throw_bad_alloc_on_open) { - throw std::bad_alloc(); - } - if (script_->throw_on_open) { - throw std::runtime_error("open raised"); - } - if (script_->open_hangs_until_stopped) { - return wait_for_stop(context); - } - if (script_->open_status != ARC_OK) { - return script_->open_status; - } - // Read the whole input through the source in small steps: proves the callbacks are honoured end to end. - uint8_t block[512]; - uint64_t offset = 0; - std::string head; - for (;;) { - uint64_t done = 0; - const arc_status_t status = source->read_at(offset, block, sizeof(block), &done); - if (status != ARC_OK) { - return status; - } - if (done == 0) { - break; - } - if (head.size() < 8) { - head.append(reinterpret_cast(block), static_cast(std::min(done, 8))); - } - offset += done; - } - script_->consumed_bytes = offset; - if (head.rfind("FAKEPDF", 0) != 0) { - return ARC_CORRUPT; - } - if (!script_->required_password.empty() && password != script_->required_password) { - return ARC_PERMISSION_DENIED; - } - if (script_->null_document) { - out.reset(); - } else { - out = std::make_unique(script_); - } - pages = script_->reported_pages != 0 ? script_->reported_pages : static_cast(script_->pages.size()); - return ARC_OK; - } - - private: - std::shared_ptr script_; -}; - -} // namespace arc::pdf::testing - -#endif diff --git a/native/shared/include/arc/arc_native_abi.h b/native/shared/include/arc/arc_native_abi.h index de8b34b7..226105fe 100644 --- a/native/shared/include/arc/arc_native_abi.h +++ b/native/shared/include/arc/arc_native_abi.h @@ -220,14 +220,6 @@ typedef struct arc_image_options_v1 { arc_limits_v1 limits; } arc_image_options_v1; -typedef struct arc_pdf_page_v1 { - uint32_t struct_size; - uint32_t struct_version; - uint32_t page_index; - uint32_t rotation; - double width_points; - double height_points; -} arc_pdf_page_v1; #pragma pack(pop) /* ABI 1.1 family declarations are shared here; family bodies/exports are delivered separately. */ @@ -248,19 +240,6 @@ ARC_ABI_EXPORT arc_status_t ARC_ABI_CALL arc_image_open(const arc_io_v1* io, con ARC_ABI_EXPORT arc_status_t ARC_ABI_CALL arc_image_read(arc_handle_t image, const arc_region_v1* region, arc_mut_buffer_t* pixels, const arc_cancel_token_t* cancel); ARC_ABI_EXPORT arc_status_t ARC_ABI_CALL arc_image_close(arc_handle_t image); -ARC_ABI_EXPORT arc_status_t ARC_ABI_CALL arc_pdf_open(const arc_io_v1* io, arc_string_view_t password, - const arc_limits_v1* limits, arc_handle_t* document, - uint32_t* pages, const arc_cancel_token_t* cancel); -ARC_ABI_EXPORT arc_status_t ARC_ABI_CALL arc_pdf_page_info(arc_handle_t document, uint32_t index, - arc_pdf_page_v1* page); -ARC_ABI_EXPORT arc_status_t ARC_ABI_CALL arc_pdf_render(arc_handle_t document, const arc_pdf_page_v1* page, - const arc_region_v1* region, uint32_t full_width, - uint32_t full_height, arc_mut_buffer_t* rgba8, - const arc_cancel_token_t* cancel); -ARC_ABI_EXPORT arc_status_t ARC_ABI_CALL arc_pdf_text(arc_handle_t document, uint32_t page, uint32_t start, - uint32_t count, arc_mut_buffer_t* text_geometry, - const arc_cancel_token_t* cancel); -ARC_ABI_EXPORT arc_status_t ARC_ABI_CALL arc_pdf_close(arc_handle_t document); #ifdef __cplusplus } @@ -400,13 +379,6 @@ ARC_ABI_LAYOUT_ASSERT(offsetof(arc_image_options_v1, mip) == 12); ARC_ABI_LAYOUT_ASSERT(offsetof(arc_image_options_v1, format) == 16); ARC_ABI_LAYOUT_ASSERT(offsetof(arc_image_options_v1, reserved) == 20); ARC_ABI_LAYOUT_ASSERT(offsetof(arc_image_options_v1, limits) == 24); -ARC_ABI_LAYOUT_ASSERT(sizeof(arc_pdf_page_v1) == 32 && ARC_ABI_ALIGNOF(arc_pdf_page_v1) == 8); -ARC_ABI_LAYOUT_ASSERT(offsetof(arc_pdf_page_v1, struct_size) == 0); -ARC_ABI_LAYOUT_ASSERT(offsetof(arc_pdf_page_v1, struct_version) == 4); -ARC_ABI_LAYOUT_ASSERT(offsetof(arc_pdf_page_v1, page_index) == 8); -ARC_ABI_LAYOUT_ASSERT(offsetof(arc_pdf_page_v1, rotation) == 12); -ARC_ABI_LAYOUT_ASSERT(offsetof(arc_pdf_page_v1, width_points) == 16); -ARC_ABI_LAYOUT_ASSERT(offsetof(arc_pdf_page_v1, height_points) == 24); #endif #undef ARC_ABI_LAYOUT_ASSERT #undef ARC_ABI_ALIGNOF diff --git a/native/shared/tests/arc_native_abi_c17_layout.c b/native/shared/tests/arc_native_abi_c17_layout.c index 56614676..b2231455 100644 --- a/native/shared/tests/arc_native_abi_c17_layout.c +++ b/native/shared/tests/arc_native_abi_c17_layout.c @@ -19,15 +19,6 @@ typedef arc_status_t(ARC_ABI_CALL* arc_image_open_signature_t)(const arc_io_v1*, typedef arc_status_t(ARC_ABI_CALL* arc_image_read_signature_t)(arc_handle_t, const arc_region_v1*, arc_mut_buffer_t*, const arc_cancel_token_t*); typedef arc_status_t(ARC_ABI_CALL* arc_image_close_signature_t)(arc_handle_t); -typedef arc_status_t(ARC_ABI_CALL* arc_pdf_open_signature_t)(const arc_io_v1*, arc_string_view_t, const arc_limits_v1*, - arc_handle_t*, uint32_t*, const arc_cancel_token_t*); -typedef arc_status_t(ARC_ABI_CALL* arc_pdf_page_info_signature_t)(arc_handle_t, uint32_t, arc_pdf_page_v1*); -typedef arc_status_t(ARC_ABI_CALL* arc_pdf_render_signature_t)(arc_handle_t, const arc_pdf_page_v1*, - const arc_region_v1*, uint32_t, uint32_t, - arc_mut_buffer_t*, const arc_cancel_token_t*); -typedef arc_status_t(ARC_ABI_CALL* arc_pdf_text_signature_t)(arc_handle_t, uint32_t, uint32_t, uint32_t, - arc_mut_buffer_t*, const arc_cancel_token_t*); -typedef arc_status_t(ARC_ABI_CALL* arc_pdf_close_signature_t)(arc_handle_t); #define ARC_ABI_REQUIRE_SIGNATURE(function_name, signature_type) \ _Static_assert(_Generic(&(function_name), signature_type: 1, default: 0), #function_name " signature mismatch") @@ -41,11 +32,6 @@ ARC_ABI_REQUIRE_SIGNATURE(arc_instruments_close, arc_instruments_close_signature ARC_ABI_REQUIRE_SIGNATURE(arc_image_open, arc_image_open_signature_t); ARC_ABI_REQUIRE_SIGNATURE(arc_image_read, arc_image_read_signature_t); ARC_ABI_REQUIRE_SIGNATURE(arc_image_close, arc_image_close_signature_t); -ARC_ABI_REQUIRE_SIGNATURE(arc_pdf_open, arc_pdf_open_signature_t); -ARC_ABI_REQUIRE_SIGNATURE(arc_pdf_page_info, arc_pdf_page_info_signature_t); -ARC_ABI_REQUIRE_SIGNATURE(arc_pdf_render, arc_pdf_render_signature_t); -ARC_ABI_REQUIRE_SIGNATURE(arc_pdf_text, arc_pdf_text_signature_t); -ARC_ABI_REQUIRE_SIGNATURE(arc_pdf_close, arc_pdf_close_signature_t); #undef ARC_ABI_REQUIRE_SIGNATURE diff --git a/native/shared/tests/arc_native_abi_cpp20_tests.cpp b/native/shared/tests/arc_native_abi_cpp20_tests.cpp index 4461a587..971a19fc 100644 --- a/native/shared/tests/arc_native_abi_cpp20_tests.cpp +++ b/native/shared/tests/arc_native_abi_cpp20_tests.cpp @@ -28,15 +28,6 @@ using arc_image_open_signature_t = arc_status_t(ARC_ABI_CALL*)(const arc_io_v1*, using arc_image_read_signature_t = arc_status_t(ARC_ABI_CALL*)(arc_handle_t, const arc_region_v1*, arc_mut_buffer_t*, const arc_cancel_token_t*); using arc_image_close_signature_t = arc_status_t(ARC_ABI_CALL*)(arc_handle_t); -using arc_pdf_open_signature_t = arc_status_t(ARC_ABI_CALL*)(const arc_io_v1*, arc_string_view_t, const arc_limits_v1*, - arc_handle_t*, uint32_t*, const arc_cancel_token_t*); -using arc_pdf_page_info_signature_t = arc_status_t(ARC_ABI_CALL*)(arc_handle_t, uint32_t, arc_pdf_page_v1*); -using arc_pdf_render_signature_t = arc_status_t(ARC_ABI_CALL*)(arc_handle_t, const arc_pdf_page_v1*, - const arc_region_v1*, uint32_t, uint32_t, - arc_mut_buffer_t*, const arc_cancel_token_t*); -using arc_pdf_text_signature_t = arc_status_t(ARC_ABI_CALL*)(arc_handle_t, uint32_t, uint32_t, uint32_t, - arc_mut_buffer_t*, const arc_cancel_token_t*); -using arc_pdf_close_signature_t = arc_status_t(ARC_ABI_CALL*)(arc_handle_t); static_assert(std::is_same_v); static_assert(std::is_same_v); @@ -47,11 +38,6 @@ static_assert(std::is_same_v); static_assert(std::is_same_v); static_assert(std::is_same_v); -static_assert(std::is_same_v); -static_assert(std::is_same_v); -static_assert(std::is_same_v); -static_assert(std::is_same_v); -static_assert(std::is_same_v); int failures = 0; diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox.Broker/ContentSandboxInvocation.cs b/src/DesktopHelpers/ArcForges.ContentSandbox.Broker/ContentSandboxInvocation.cs index 30c3d569..21d2d058 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox.Broker/ContentSandboxInvocation.cs +++ b/src/DesktopHelpers/ArcForges.ContentSandbox.Broker/ContentSandboxInvocation.cs @@ -37,7 +37,6 @@ public sealed class ContentSandboxInvocation : IAsyncDisposable private readonly SemaphoreSlim _operations = new(1, 1); private readonly CancellationTokenSource _lifetime = new(); private readonly Dictionary _images = []; - private readonly Dictionary _documents = []; private readonly Guid _invocationId; private readonly Guid _leaseId; private readonly ulong _generation = 1; @@ -339,202 +338,6 @@ await RunAsync( }, cancellationToken).ConfigureAwait(false); - /// Opens the input as a PDF document. The document keeps the identifier the parent mints here. - /// Cancels the call, and with it the invocation. - public async ValueTask> OpenPdfAsync(CancellationToken cancellationToken = default) => - await RunAsync( - async token => - { - var documentId = Guid.NewGuid(); - var response = await _client.OpenPdfAsync( - new ContentSandboxServiceOpenPdfRequest - { - Meta = NewMeta(), - SessionId = SandboxRecords.ToWireId(_sessionId), - DocumentId = SandboxRecords.ToWireId(documentId), - }, - CallOptions(token)).ConfigureAwait(false); - if (response.OutcomeCase != ContentSandboxServiceOpenPdfResponse.OutcomeOneofCase.Value) - { - return Failed(response.Error); - } - - if (!SandboxRecords.TryReadId(response.Value.DocumentId, out var echoed) || echoed != documentId || !response.Value.HasPageCount - || response.Value.PageCount > _options.Limits.MaxItems) - { - return await EndedBecauseAsync("resource.parser_failed").ConfigureAwait(false); - } - - _documents[documentId] = response.Value.PageCount; - return ContentSandboxResult.Ok(documentId); - }, - cancellationToken).ConfigureAwait(false); - - /// Reads the geometry of one page of an opened document. - /// The identifier returned by . - /// The zero-based page. - /// Cancels the call, and with it the invocation. - public async ValueTask> GetPdfPageAsync(Guid documentId, uint pageIndex, CancellationToken cancellationToken = default) => - await RunAsync( - async token => - { - if (!_documents.TryGetValue(documentId, out var pages) || pageIndex >= pages) - { - return ContentSandboxResult.Fail("state.not_found"); - } - - var response = await _client.GetPdfPageAsync( - new ContentSandboxServiceGetPdfPageRequest - { - Meta = NewMeta(), - SessionId = SandboxRecords.ToWireId(_sessionId), - DocumentId = SandboxRecords.ToWireId(documentId), - PageIndex = pageIndex, - }, - CallOptions(token)).ConfigureAwait(false); - if (response.OutcomeCase != ContentSandboxServiceGetPdfPageResponse.OutcomeOneofCase.Value) - { - return Failed(response.Error); - } - - var page = response.Value.Page; - if (!SandboxProfileCheck.IsValid(page) || page.PageIndex != pageIndex) - { - return await EndedBecauseAsync("resource.parser_failed").ConfigureAwait(false); - } - - return ContentSandboxResult.Ok(page); - }, - cancellationToken).ConfigureAwait(false); - - /// Extracts a bounded chunk of page text with UTF-16 positions; start is 0 or the next position a previous chunk returned. - /// The identifier returned by . - /// The zero-based page. - /// The UTF-16 position to start at. - /// Cancels the call, and with it the invocation. - public async ValueTask> ExtractPdfTextAsync(Guid documentId, uint pageIndex, uint start, CancellationToken cancellationToken = default) => - await RunAsync( - async token => - { - if (!_documents.TryGetValue(documentId, out var pages) || pageIndex >= pages) - { - return ContentSandboxResult.Fail("state.not_found"); - } - - var response = await _client.ExtractPdfTextAsync( - new ContentSandboxServiceExtractPdfTextRequest - { - Meta = NewMeta(), - SessionId = SandboxRecords.ToWireId(_sessionId), - DocumentId = SandboxRecords.ToWireId(documentId), - PageIndex = pageIndex, - Start = start, - }, - CallOptions(token)).ConfigureAwait(false); - if (response.OutcomeCase != ContentSandboxServiceExtractPdfTextResponse.OutcomeOneofCase.Value) - { - return Failed(response.Error); - } - - var text = response.Value.Text; - if (!SandboxProfileCheck.IsValid(response) || text.PageIndex != pageIndex || text.Start != start - || (uint)text.Boxes.Count > _options.Limits.MaxItems || (text.HasNext && text.Next <= start)) - { - return await EndedBecauseAsync("resource.parser_failed").ConfigureAwait(false); - } - - return ContentSandboxResult.Ok(text); - }, - cancellationToken).ConfigureAwait(false); - - /// Renders one tile of a page through a brokered output slot, with the same checks as . - /// The identifier returned by . - /// The page geometry exactly as returned it. - /// The full rendered width in pixels. - /// The full rendered height in pixels. - /// The tile origin. - /// The tile origin. - /// The tile width in pixels, 1 through 2048. - /// The tile height in pixels, 1 through 2048. - /// Cancels the call, and with it the invocation. - public async ValueTask> RenderPdfTileAsync( - Guid documentId, - SandboxPdfPage page, - uint fullWidth, - uint fullHeight, - uint x, - uint y, - uint width, - uint height, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(page); - return await RunAsync( - token => - { - if (!_documents.TryGetValue(documentId, out var pages) || !page.HasPageIndex || page.PageIndex >= pages || !SandboxProfileCheck.IsValid(page)) - { - return ValueTask.FromResult(ContentSandboxResult.Fail("state.not_found")); - } - - return TileCoreAsync( - 1, - fullWidth, - fullHeight, - x, - y, - width, - height, - async (grant, region, call) => - { - var response = await _client.RenderPdfTileAsync( - new ContentSandboxServiceRenderPdfTileRequest - { - Meta = NewMeta(), - SessionId = SandboxRecords.ToWireId(_sessionId), - DocumentId = SandboxRecords.ToWireId(documentId), - Page = page, - Region = region, - Grant = grant, - FullWidth = fullWidth, - FullHeight = fullHeight, - }, - call).ConfigureAwait(false); - return response.OutcomeCase == ContentSandboxServiceRenderPdfTileResponse.OutcomeOneofCase.Value - ? (response.Value.Buffer, null) - : (null, response.Error); - }, - token); - }, - cancellationToken).ConfigureAwait(false); - } - - /// Closes an opened document. Closing an unknown document is a typed refusal. - /// The identifier returned by . - /// Cancels the call, and with it the invocation. - public async ValueTask> ClosePdfAsync(Guid documentId, CancellationToken cancellationToken = default) => - await RunAsync( - async token => - { - if (!_documents.Remove(documentId)) - { - return ContentSandboxResult.Fail("state.not_found"); - } - - var response = await _client.ClosePdfAsync( - new ContentSandboxServiceClosePdfRequest - { - Meta = NewMeta(), - SessionId = SandboxRecords.ToWireId(_sessionId), - DocumentId = SandboxRecords.ToWireId(documentId), - }, - CallOptions(token)).ConfigureAwait(false); - return response.OutcomeCase == ContentSandboxServiceClosePdfResponse.OutcomeOneofCase.Value - ? ContentSandboxResult.Ok(true) - : Failed(response.Error); - }, - cancellationToken).ConfigureAwait(false); - /// /// Cancels the invocation: every grant is withdrawn, in-flight calls are abandoned and the helper is asked to stop through the reserved /// cancellation control slot, which works even when the data lane is full. A helper that does not stop within five seconds is terminated. diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj b/src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj index daf4e45e..5f6d1f4c 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj @@ -2,7 +2,7 @@ @@ -22,7 +22,6 @@ - AGPL diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/HostileTestParser.cs b/src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/HostileTestParser.cs index e243b65e..30f6eeb4 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/HostileTestParser.cs +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/HostileTestParser.cs @@ -31,9 +31,6 @@ internal sealed class HostileProfile : IContentParserProfile /// public IImageParser? CreateImageParser() => new HostileImageParser(); - - /// - public IPdfParser? CreatePdfParser() => new HostilePdfParser(); } /// The script a hostile test document carries: line-oriented, first line the magic, the rest commands. @@ -144,91 +141,6 @@ public void Dispose() } } -/// The PDF side of the hostile fixture: pages and text come from the script, never from a real document. -internal sealed class HostilePdfParser : IPdfParser -{ - private readonly List<(double Width, double Height, string Text)> _pages = []; - - /// - public uint Open(ParserInput input, ParserContext context) - { - ArgumentNullException.ThrowIfNull(context); - var script = HostileScript.Read(input); - foreach (var line in script.Lines) - { - if (line[0] == "page" && line.Length >= 3 - && double.TryParse(line[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var width) - && double.TryParse(line[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var height)) - { - _pages.Add((width, height, string.Join(' ', line.Skip(3)))); - } - else if (line[0] == "longpage" && line.Length >= 2 && int.TryParse(line[1], NumberStyles.None, CultureInfo.InvariantCulture, out var chars)) - { - _pages.Add((612, 792, LongText(chars))); - } - } - - HostileBehaviors.OnOpen(script, context); - return (uint)_pages.Count; - } - - /// - public SandboxPdfPage GetPage(uint pageIndex, ParserContext context) - { - var page = _pages[(int)pageIndex]; - return new SandboxPdfPage { PageIndex = pageIndex, Rotation = 0, WidthPoints = page.Width, HeightPoints = page.Height }; - } - - /// - public PdfPageText GetPageText(uint pageIndex, ParserContext context) - { - var text = _pages[(int)pageIndex].Text; - var boxes = new List(); - var start = 0; - while (start < text.Length) - { - var end = text.IndexOf(' ', start); - end = end < 0 ? text.Length : end; - if (end > start) - { - boxes.Add(new SandboxTextBox { Start = (uint)start, Length = (uint)(end - start), X = start, Y = 0, Width = end - start, Height = 10 }); - } - - start = end + 1; - } - - return new PdfPageText(text, boxes); - } - - /// - public int RenderTile(SandboxPdfPage page, SandboxRegion region, uint fullWidth, uint fullHeight, Span destination, ParserContext context) => - TilePattern.Fill(region, 1, fullWidth, fullHeight, destination); - - /// - public void Dispose() - { - } - - private static string LongText(int chars) - { - var builder = new StringBuilder(chars); - var word = 0; - while (builder.Length < chars) - { - // Words of letters, a four-byte character (a surrogate pair) every few words, and spaces, so that a cut can land inside a pair. - builder.Append("word").Append(word.ToString(CultureInfo.InvariantCulture)).Append(' '); - if (word % 5 == 0) - { - builder.Append(char.ConvertFromUtf32(0x1F600)).Append(' '); - } - - word++; - } - - return builder.ToString(0, chars).TrimEnd(char.ConvertFromUtf32(0x1F600)[0]); - } -} - /// The deterministic tile content: it makes a wrong tile, a wrong slot or a stale buffer visible. internal static class TilePattern { diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/packages.lock.json b/src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/packages.lock.json index 23322f5e..3f9ebc6f 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/packages.lock.json +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/Fixture/packages.lock.json @@ -56,8 +56,7 @@ "dependencies": { "ArcForges.ContentSandbox.Contracts": "[1.0.0, )", "ArcForges.Foundation": "[1.0.0, )", - "ArcForges.LocalRpc": "[1.0.0, )", - "ArcForges.Native.Pdf": "[1.0.0, )" + "ArcForges.LocalRpc": "[1.0.0, )" } }, "arcforges.contentsandbox.contracts": { @@ -81,15 +80,6 @@ "Grpc.Net.Client": "[2.84.0, )" } }, - "arcforges.native.abstractions": { - "type": "Project" - }, - "arcforges.native.pdf": { - "type": "Project", - "dependencies": { - "ArcForges.Native.Abstractions": "[1.0.0, )" - } - }, "ArcForges.Contracts.Foundation": { "type": "CentralTransitive", "requested": "[1.0.0-ci.216.1, )", diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Host/ContentSandboxServiceImpl.cs b/src/DesktopHelpers/ArcForges.ContentSandbox/Host/ContentSandboxServiceImpl.cs index 1a071a23..67a13bcf 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Host/ContentSandboxServiceImpl.cs +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/Host/ContentSandboxServiceImpl.cs @@ -23,7 +23,6 @@ namespace ArcForges.ContentSandbox.Host; internal class ContentSandboxServiceImpl : ContentSandboxService.ContentSandboxServiceBase, IDisposable { private const int MaxOpenObjects = 8; - private const int MaxPdfTextBytes = 60 * 1024; private const int RememberedRenewals = 16; private static readonly TimeSpan SessionLease = TimeSpan.FromSeconds(30); @@ -423,279 +422,71 @@ public override Task CloseImage(Content } /// - public override async Task OpenPdf(ContentSandboxServiceOpenPdfRequest request, ServerCallContext context) + /// The local PDF path is retired (P2-022). Each PDF call is checked as before and answered with its closed refusal, or with its receipt, without any parser. + public override Task OpenPdf(ContentSandboxServiceOpenPdfRequest request, ServerCallContext context) { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(context); var response = new ContentSandboxServiceOpenPdfResponse { Meta = MetaOf(request.Meta) }; - if (!Shape.IsValid(request) || !TryGetLiveSession(request.SessionId, out var session) || !SandboxRecords.TryReadId(request.DocumentId, out var documentId)) - { - response.Error = Failure("validation.invalid_request"); - return response; - } - - PdfState? existing; - lock (_gate) - { - _ = session.Documents.TryGetValue(documentId, out existing); - if (existing is null && session.Documents.Count >= MaxOpenObjects) - { - response.Error = Failure("capacity.busy"); - return response; - } - } - - if (existing is not null) - { - response.Value = new ContentSandboxServiceOpenPdfValue { DocumentId = SandboxRecords.ToWireId(documentId), PageCount = existing.PageCount }; - return response; - } - - var parser = _profile?.CreatePdfParser(); - if (parser is null) - { - response.Error = Failure("resource.unavailable"); - return response; - } - - var input = new ParserInput(_resources.Input, (long)_frame.InputLength); - var opened = await RunParserAsync(session, parserContext => parser.Open(input, parserContext), context.CancellationToken).ConfigureAwait(false); - if (opened.Failure is not null) - { - parser.Dispose(); - response.Error = Failure(opened.Failure); - return response; - } - - if (opened.Value > session.Limits.MaxItems) - { - parser.Dispose(); - response.Error = Failure("resource.parser_failed"); - return response; - } - - lock (_gate) - { - session.Documents[documentId] = new PdfState(parser, opened.Value); - } - - response.Value = new ContentSandboxServiceOpenPdfValue { DocumentId = SandboxRecords.ToWireId(documentId), PageCount = opened.Value }; - return response; + response.Error = PdfCallIsLive(request.SessionId, request.DocumentId, Shape.IsValid(request)) + ? Failure("resource.parser_failed") + : Failure("validation.invalid_request"); + return Task.FromResult(response); } /// - public override async Task GetPdfPage(ContentSandboxServiceGetPdfPageRequest request, ServerCallContext context) + /// Retired with the PDF path (P2-022): no document can be opened, so no page exists to return. + public override Task GetPdfPage(ContentSandboxServiceGetPdfPageRequest request, ServerCallContext context) { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(context); var response = new ContentSandboxServiceGetPdfPageResponse { Meta = MetaOf(request.Meta) }; - if (!Shape.IsValid(request) || !TryGetLiveSession(request.SessionId, out var session) || !SandboxRecords.TryReadId(request.DocumentId, out var documentId)) - { - response.Error = Failure("validation.invalid_request"); - return response; - } - - PdfState? document; - lock (_gate) - { - _ = session.Documents.TryGetValue(documentId, out document); - } - - if (document is null || request.PageIndex >= document.PageCount) - { - response.Error = Failure("state.not_found"); - return response; - } - - var result = await RunParserAsync(session, parserContext => document.Parser.GetPage(request.PageIndex, parserContext), context.CancellationToken).ConfigureAwait(false); - if (result.Failure is not null) - { - response.Error = Failure(result.Failure); - return response; - } - - var page = result.Value!; - if (!SandboxProfileCheck.IsValid(page) || page.PageIndex != request.PageIndex) - { - response.Error = Failure("resource.parser_failed"); - return response; - } - - lock (_gate) - { - document.Pages[request.PageIndex] = page.Clone(); - } - - response.Value = new ContentSandboxServiceGetPdfPageValue { Page = page }; - return response; + response.Error = PdfCallIsLive(request.SessionId, request.DocumentId, Shape.IsValid(request)) + ? Failure("state.not_found") + : Failure("validation.invalid_request"); + return Task.FromResult(response); } /// - public override async Task ExtractPdfText(ContentSandboxServiceExtractPdfTextRequest request, ServerCallContext context) + /// Retired with the PDF path (P2-022): no document can be opened, so no text exists to return. + public override Task ExtractPdfText(ContentSandboxServiceExtractPdfTextRequest request, ServerCallContext context) { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(context); var response = new ContentSandboxServiceExtractPdfTextResponse { Meta = MetaOf(request.Meta) }; - if (!Shape.IsValid(request) || !TryGetLiveSession(request.SessionId, out var session) || !SandboxRecords.TryReadId(request.DocumentId, out var documentId)) - { - response.Error = Failure("validation.invalid_request"); - return response; - } - - PdfState? document; - lock (_gate) - { - _ = session.Documents.TryGetValue(documentId, out document); - } - - if (document is null || request.PageIndex >= document.PageCount) - { - response.Error = Failure("state.not_found"); - return response; - } - - PdfPageText? pageText; - lock (_gate) - { - _ = document.Texts.TryGetValue(request.PageIndex, out pageText); - } - - if (pageText is null) - { - var result = await RunParserAsync(session, parserContext => document.Parser.GetPageText(request.PageIndex, parserContext), context.CancellationToken).ConfigureAwait(false); - if (result.Failure is not null) - { - response.Error = Failure(result.Failure); - return response; - } - - pageText = result.Value!; - if (pageText.Text.Length > 16 * 1024 * 1024 || pageText.Boxes.Any(box => !IsFinite(box))) - { - response.Error = Failure("resource.parser_failed"); - return response; - } - - lock (_gate) - { - document.Texts[request.PageIndex] = pageText; - } - } - - HashSet allowed; - lock (_gate) - { - if (!document.Starts.TryGetValue(request.PageIndex, out allowed!)) - { - allowed = [0]; - document.Starts[request.PageIndex] = allowed; - } - } - - bool permitted; - lock (_gate) - { - permitted = allowed.Contains(request.Start); - } - - if (!permitted) - { - response.Error = Failure("validation.invalid_offset"); - return response; - } - - var chunk = PdfTextPager.Cut(request.PageIndex, request.Start, pageText, session.Limits.MaxItems, MaxPdfTextBytes); - if (chunk is null) - { - response.Error = Failure("resource.parser_failed"); - return response; - } - - response.Value = new ContentSandboxServiceExtractPdfTextValue { Text = chunk }; - if (!SandboxProfileCheck.IsValid(response)) - { - response.Value = null; - response.Error = Failure("resource.parser_failed"); - return response; - } - - if (chunk.HasNext) - { - lock (_gate) - { - _ = allowed.Add(chunk.Next); - } - } - - return response; + response.Error = PdfCallIsLive(request.SessionId, request.DocumentId, Shape.IsValid(request)) + ? Failure("state.not_found") + : Failure("validation.invalid_request"); + return Task.FromResult(response); } /// - public override async Task RenderPdfTile(ContentSandboxServiceRenderPdfTileRequest request, ServerCallContext context) + /// Retired with the PDF path (P2-022): no document can be opened, so no tile can be rendered. + public override Task RenderPdfTile(ContentSandboxServiceRenderPdfTileRequest request, ServerCallContext context) { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(context); var response = new ContentSandboxServiceRenderPdfTileResponse { Meta = MetaOf(request.Meta) }; - if (!Shape.IsValid(request) || !TryGetLiveSession(request.SessionId, out var session) || !SandboxRecords.TryReadId(request.DocumentId, out var documentId) - || !SandboxProfileCheck.IsValid(request.Page) || !SandboxProfileCheck.IsValid(request.Region) - || request.FullWidth == 0 || request.FullHeight == 0) - { - response.Error = Failure("validation.invalid_request"); - return response; - } - - PdfState? document; - SandboxPdfPage? known = null; - lock (_gate) - { - _ = session.Documents.TryGetValue(documentId, out document); - _ = document?.Pages.TryGetValue(request.Page.PageIndex, out known); - } - - // The page geometry must be the one GetPdfPage returned; scaling is never inferred from untrusted fields. - if (document is null || known is null || !known.Equals(request.Page)) - { - response.Error = Failure(document is null ? "state.not_found" : "validation.invalid_request"); - return response; - } - - var tile = await RenderTileAsync( - session, - request.Region, - request.Grant, - 1, - request.FullWidth, - request.FullHeight, - (region, span, parserContext) => document.Parser.RenderTile(request.Page, region, request.FullWidth, request.FullHeight, span, parserContext), - context.CancellationToken).ConfigureAwait(false); - if (tile.Error is not null) - { - response.Error = tile.Error; - return response; - } - - response.Value = new ContentSandboxServiceRenderPdfTileValue { Buffer = tile.Descriptor }; - return response; + var wellFormed = Shape.IsValid(request) && SandboxProfileCheck.IsValid(request.Page) && SandboxProfileCheck.IsValid(request.Region) + && request.FullWidth != 0 && request.FullHeight != 0; + response.Error = PdfCallIsLive(request.SessionId, request.DocumentId, wellFormed) + ? Failure("state.not_found") + : Failure("validation.invalid_request"); + return Task.FromResult(response); } /// + /// Retired with the PDF path (P2-022): closing a document that was never opened is the same idempotent receipt as before. public override Task ClosePdf(ContentSandboxServiceClosePdfRequest request, ServerCallContext context) { ArgumentNullException.ThrowIfNull(request); var response = new ContentSandboxServiceClosePdfResponse { Meta = MetaOf(request.Meta) }; - if (!Shape.IsValid(request) || !TryGetLiveSession(request.SessionId, out var session) || !SandboxRecords.TryReadId(request.DocumentId, out var documentId)) + if (!PdfCallIsLive(request.SessionId, request.DocumentId, Shape.IsValid(request))) { response.Error = Failure("validation.invalid_request"); return Task.FromResult(response); } - PdfState? document; - lock (_gate) - { - _ = session.Documents.Remove(documentId, out document); - } - - document?.Parser.Dispose(); response.Value = new ContentSandboxServiceClosePdfValue { Receipt = ReceiptOf(request.Meta) }; return Task.FromResult(response); } @@ -782,9 +573,6 @@ LocalRpcBrokerRefusal.CapacityExceeded or LocalRpcBrokerRefusal.UnknownSlot or L private long Ticks(TimeSpan span) => (long)(span.TotalSeconds * _clock.TimestampFrequency); - private static bool IsFinite(SandboxTextBox box) => - double.IsFinite(box.X) && double.IsFinite(box.Y) && double.IsFinite(box.Width) && double.IsFinite(box.Height); - private bool TryGetSession(Id? wire, [NotNullWhen(true)] out SessionState? session) { session = null; @@ -800,6 +588,13 @@ private bool TryGetSession(Id? wire, [NotNullWhen(true)] out SessionState? sessi return false; } + /// + /// The request is well formed and names the live session and a well-formed document identifier. Retired PDF calls use it only to choose + /// between the validation refusal and their closed answer: no document can exist, so no PDF parser is reached. + /// + private bool PdfCallIsLive(Id? sessionId, Id? documentId, bool shapeIsValid) => + shapeIsValid && TryGetLiveSession(sessionId, out _) && SandboxRecords.TryReadId(documentId, out _); + private bool TryGetLiveSession(Id? wire, [NotNullWhen(true)] out SessionState? session) { if (!TryGetSession(wire, out session)) @@ -839,9 +634,7 @@ private void CloseSessionState(SessionState session) session.Closed = true; parsers.AddRange(session.Images.Values.Select(image => image.Parser)); - parsers.AddRange(session.Documents.Values.Select(document => document.Parser)); session.Images.Clear(); - session.Documents.Clear(); } session.Cancelled.Cancel(); @@ -984,19 +777,6 @@ internal bool Matches(uint otherSubimage, uint otherMip, uint otherFormat) => Subimage == otherSubimage && Mip == otherMip && Format == otherFormat; } - private sealed class PdfState(IPdfParser parser, uint pageCount) - { - internal IPdfParser Parser { get; } = parser; - - internal uint PageCount { get; } = pageCount; - - internal Dictionary Pages { get; } = []; - - internal Dictionary Texts { get; } = []; - - internal Dictionary> Starts { get; } = []; - } - private sealed class SessionState { internal Guid SessionId { get; init; } @@ -1017,8 +797,6 @@ private sealed class SessionState internal Dictionary Images { get; } = []; - internal Dictionary Documents { get; } = []; - internal Dictionary Renewals { get; } = []; internal Queue RenewalOrder { get; } = new(); diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Host/NativePdfParser.cs b/src/DesktopHelpers/ArcForges.ContentSandbox/Host/NativePdfParser.cs deleted file mode 100644 index 66de9d2a..00000000 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Host/NativePdfParser.cs +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -using System.Diagnostics.CodeAnalysis; -using System.Text; -using ArcForges.Contracts.LocalRpc.Sandbox.V1; -using ArcForges.Native.Pdf; - -namespace ArcForges.ContentSandbox.Host; - -/// Opens a document of the native PDF library. The production opener is ; tests substitute a scripted one. -internal delegate IPdfDocument PdfDocumentOpener(IPdfInput input, PdfLimits limits, CancellationToken cancellation); - -/// -/// The production PDF parser of the helper: a thin, validating adapter from the sandbox parser interface to the native -/// arc_pdf_* library. It runs only inside the helper process under its restricted profile. It adds no format knowledge of its own; it -/// enforces the budget of the launch on every result it takes from the native side (geometry, page text volume, box count) and turns every -/// native failure, whatever its type, into so that nothing a parser says reaches the parent as prose. -/// -internal sealed class NativePdfParser(PdfDocumentOpener opener) : IPdfParser -{ - /// The most UTF-16 units one native text call is asked for (the native limit). - internal const uint TextUnitsPerCall = 65536; - - /// The most text of one page the helper assembles (the host rejects more as well). - internal const int MaxPageTextUnits = 16 * 1024 * 1024; - - /// The most boxes of one page the helper assembles. - internal const int MaxPageBoxes = 1_000_000; - - private readonly object _gate = new(); - private IPdfDocument? _document; - - /// - public uint Open(ParserInput input, ParserContext context) - { - ArgumentNullException.ThrowIfNull(input); - ArgumentNullException.ThrowIfNull(context); - if (_document is not null) - { - throw new ContentParserException("The parser holds a document already."); - } - - var limits = context.Limits; - var native = new PdfLimits(limits.MaxInputBytes, limits.MaxMemoryBytes, limits.MaxOutputBytes, limits.MaxWidth, limits.MaxHeight, limits.MaxItems, limits.TimeoutMs); - var document = Translate(() => opener(new ParserInputAdapter(input), native, context.Cancelled)); - if (document.PageCount == 0) - { - document.Dispose(); - throw new ContentParserException("The document has no pages."); - } - - lock (_gate) - { - _document = document; - } - - return document.PageCount; - } - - /// - public SandboxPdfPage GetPage(uint pageIndex, ParserContext context) - { - var document = Document(); - var page = Translate(() => document.GetPage(pageIndex)); - if (page.PageIndex != pageIndex || !double.IsFinite(page.WidthPoints) || !double.IsFinite(page.HeightPoints) || page.WidthPoints <= 0 || page.HeightPoints <= 0 - || page.Rotation is not (0 or 90 or 180 or 270)) - { - throw new ContentParserException("The page geometry is not valid."); - } - - return new SandboxPdfPage { PageIndex = page.PageIndex, Rotation = page.Rotation, WidthPoints = page.WidthPoints, HeightPoints = page.HeightPoints }; - } - - /// - public PdfPageText GetPageText(uint pageIndex, ParserContext context) - { - ArgumentNullException.ThrowIfNull(context); - var document = Document(); - var text = new StringBuilder(); - var boxes = new List(); - uint start = 0; - while (true) - { - context.Cancelled.ThrowIfCancellationRequested(); - var chunk = Translate(() => document.ExtractText(pageIndex, start, TextUnitsPerCall, context.Cancelled)); - if (chunk.Page != pageIndex || chunk.Start != start || text.Length != start) - { - throw new ContentParserException("The text chunk answers another position."); - } - - if ((long)text.Length + chunk.Text.Length > MaxPageTextUnits || boxes.Count + chunk.Boxes.Count > MaxPageBoxes) - { - throw new ContentParserException("The page text exceeds its bound."); - } - - _ = text.Append(chunk.Text); - foreach (var box in chunk.Boxes) - { - boxes.Add(new SandboxTextBox { Start = box.Start, Length = box.Length, X = box.X, Y = box.Y, Width = box.Width, Height = box.Height }); - } - - if (chunk.Next is not { } next) - { - break; - } - - if (next <= start || next != text.Length) - { - throw new ContentParserException("The text chunk makes no progress."); - } - - start = next; - } - - return new PdfPageText(text.ToString(), boxes); - } - - /// - public int RenderTile(SandboxPdfPage page, SandboxRegion region, uint fullWidth, uint fullHeight, Span destination, ParserContext context) - { - ArgumentNullException.ThrowIfNull(page); - ArgumentNullException.ThrowIfNull(region); - ArgumentNullException.ThrowIfNull(context); - if (region.RowStride > uint.MaxValue) - { - throw new ContentParserException("The row stride is out of range."); - } - - var document = Document(); - var geometry = new PdfPageGeometry(page.PageIndex, page.Rotation, page.WidthPoints, page.HeightPoints); - var stride = (uint)region.RowStride; - var token = context.Cancelled; - - // A Span cannot cross a lambda; the native call is made directly and its failures translated by the same rules. - try - { - var written = document.Render(geometry, region.X, region.Y, region.Width, region.Height, fullWidth, fullHeight, stride, destination, token); - return written < 0 || written > destination.Length ? throw new ContentParserException("The native render reported an impossible size.") : written; - } - catch (Exception exception) when (IsNativeFailure(exception)) - { - throw new ContentParserException("The native render failed.", exception); - } - } - - /// - public void Dispose() - { - lock (_gate) - { - _document?.Dispose(); - _document = null; - } - } - - private IPdfDocument Document() => _document ?? throw new ContentParserException("No document is open."); - - [SuppressMessage("Design", "CA1031", Justification = "Every native or loader failure maps to the single parser failure; the text is diagnostic and never sent.")] - private static T Translate(Func call) - { - try - { - return call(); - } - catch (Exception exception) when (IsNativeFailure(exception)) - { - throw new ContentParserException("The native PDF library failed.", exception); - } - } - - private static bool IsNativeFailure(Exception exception) => - exception is PdfNativeException or DllNotFoundException or EntryPointNotFoundException or BadImageFormatException or PlatformNotSupportedException - or InvalidDataException or IOException or ObjectDisposedException or ArgumentException or InvalidOperationException or OverflowException; - - private sealed class ParserInputAdapter(ParserInput input) : IPdfInput - { - public long Length => input.Length; - - public int ReadAt(long offset, Span destination) => input.Read(offset, destination); - } -} diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Host/Parsers.cs b/src/DesktopHelpers/ArcForges.ContentSandbox/Host/Parsers.cs index f41f6d6a..7277dc67 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Host/Parsers.cs +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/Host/Parsers.cs @@ -57,11 +57,6 @@ internal int Read(long offset, Span destination) /// Cancelled on session cancellation, expiry and parent loss. internal sealed record ParserContext(ContentSandboxLimits Limits, CancellationToken Cancelled); -/// The full text of one page with finite UTF-16 geometry boxes. The host paginates it into bounded chunks. -/// The page text. -/// Boxes whose start and length are UTF-16 offsets into . -internal sealed record PdfPageText(string Text, IReadOnlyList Boxes); - /// An image parser that runs only inside the helper process under its restricted profile. internal interface IImageParser : IDisposable { @@ -72,26 +67,10 @@ internal interface IImageParser : IDisposable int ReadTile(SandboxRegion region, uint format, Span destination, ParserContext context); } -/// A PDF parser that runs only inside the helper process under its restricted profile. Scripts and actions stay disabled. -internal interface IPdfParser : IDisposable -{ - /// Opens the input and returns the page count. - uint Open(ParserInput input, ParserContext context); - - /// The geometry of one page. - SandboxPdfPage GetPage(uint pageIndex, ParserContext context); - - /// The whole text of one page. - PdfPageText GetPageText(uint pageIndex, ParserContext context); - - /// Renders the tile into and returns the number of bytes written. - int RenderTile(SandboxPdfPage page, SandboxRegion region, uint fullWidth, uint fullHeight, Span destination, ParserContext context); -} - /// /// One approved parser composition, chosen by the identifier of the launch and never by a path or a function name. The production helper -/// composes (the PDF parser over the native library); the hostile test composition exists only in the -/// test fixture. A composition that lacks a parser kind refuses that kind. +/// composes , which composes no parser until the still-image composition is delivered; the hostile test +/// composition exists only in the test fixture. A composition that lacks a parser kind refuses that kind. /// internal interface IContentParserProfile { @@ -101,9 +80,6 @@ internal interface IContentParserProfile /// A new image parser, or null when the composition has none. IImageParser? CreateImageParser(); - /// A new PDF parser, or null when the composition has none. - IPdfParser? CreatePdfParser(); - /// /// Runs before the operating-system profile is applied: loads and verifies whatever native libraries the composition needs, because a /// restricted process can no longer load them. Throws when the composition cannot run; the helper diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Host/PdfTextPager.cs b/src/DesktopHelpers/ArcForges.ContentSandbox/Host/PdfTextPager.cs deleted file mode 100644 index d904eca1..00000000 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Host/PdfTextPager.cs +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -using ArcForges.Contracts.LocalRpc.Sandbox.V1; - -namespace ArcForges.ContentSandbox.Host; - -/// -/// Cuts the text of a page into the bounded chunks of annex 09 section 5: positions are zero-based UTF-16 offsets, a chunk never splits a -/// surrogate pair, a chunk carries only the boxes that lie wholly inside it, the whole message stays under the response bound, and every chunk -/// of a non-empty remainder makes progress. Truncation is always explicit pagination through the next position. -/// -internal static class PdfTextPager -{ - private const int FirstTry = 16 * 1024; - - /// - /// The chunk of that starts at , or null when is not a position - /// a chunk can start at or no bounded chunk exists. - /// - internal static SandboxPdfText? Cut(uint pageIndex, uint start, PdfPageText page, uint maxBoxes, int maxBytes) - { - ArgumentNullException.ThrowIfNull(page); - var text = page.Text; - var length = text.Length; - if (start > length || (start > 0 && start < length && char.IsLowSurrogate(text[(int)start]) && char.IsHighSurrogate(text[(int)start - 1]))) - { - return null; - } - - var first = (int)start; - var end = (int)Math.Min((long)length, (long)first + FirstTry); - while (true) - { - end = StayOnBoundary(text, first, end); - var boxes = BoxesWithin(page.Boxes, start, (uint)end); - if (boxes.Count > maxBoxes) - { - var cut = (int)boxes[(int)maxBoxes].Start; - if (cut <= first) - { - return null; - } - - end = cut; - continue; - } - - var chunk = new SandboxPdfText - { - PageIndex = pageIndex, - Start = start, - Text = text.Substring(first, end - first), - }; - if (end < length) - { - chunk.Next = (uint)end; - } - - chunk.Boxes.AddRange(boxes); - if (chunk.CalculateSize() <= maxBytes) - { - return chunk; - } - - if (end - first <= 1) - { - return null; - } - - end = first + Math.Max(1, (end - first) / 2); - } - } - - private static int StayOnBoundary(string text, int start, int end) - { - if (end < text.Length && end > start && char.IsLowSurrogate(text[end]) && char.IsHighSurrogate(text[end - 1])) - { - end--; - } - - if (end == start && start < text.Length) - { - end = start + (char.IsHighSurrogate(text[start]) && start + 1 < text.Length && char.IsLowSurrogate(text[start + 1]) ? 2 : 1); - } - - return end; - } - - private static List BoxesWithin(IReadOnlyList boxes, uint start, uint end) - { - var result = new List(); - foreach (var box in boxes) - { - if (box.Start >= start && (ulong)box.Start + box.Length <= end) - { - result.Add(box.Clone()); - } - } - - return result; - } -} diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Host/ProductionParserProfile.cs b/src/DesktopHelpers/ArcForges.ContentSandbox/Host/ProductionParserProfile.cs index b46a34f8..1b26fd19 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Host/ProductionParserProfile.cs +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/Host/ProductionParserProfile.cs @@ -1,74 +1,20 @@ // SPDX-License-Identifier: AGPL-3.0-only -using System.Diagnostics.CodeAnalysis; -using ArcForges.Native.Abstractions; -using ArcForges.Native.Pdf; - namespace ArcForges.ContentSandbox.Host; /// -/// The approved production parser composition of the helper (WP-13.13). It composes the PDF parser over the native arc_pdf_* library -/// and nothing else: no image parser is composed here (the still-image functions of ArcImageNative do not exist yet), so a launch that opens -/// an image against this composition is refused as unavailable. The hostile test parser is never part of this composition. +/// The production parser composition of the helper. The local PDF parser path is retired (P2-022): no PDF parser is composed and no native +/// PDF library is loaded or linked. The still-image composition is delivered by NAT.31 and is not yet present, so no image parser is composed +/// either and a launch that opens an image against this composition is refused as unavailable. The hostile test parser is never part of this +/// composition. /// -/// -/// loads and verifies the native library BEFORE the operating-system profile is applied: on Linux a seccomp/Landlock -/// profile cannot load a library afterwards, and on Windows the loader must not run late inside the container. A library that has no PDF -/// backend linked (backend=none) fails the preparation, so the helper ends before any parser exists instead of failing per call. -/// -internal sealed class ProductionParserProfile(PdfDocumentOpener? opener = null, Action? prepare = null) : IContentParserProfile +internal sealed class ProductionParserProfile : IContentParserProfile { /// The identifier a launch names to select the production composition. internal const string ProfileId = "arcforges-parsers-v1"; - private readonly PdfDocumentOpener _opener = opener ?? OpenNative; - private readonly Action _prepare = prepare ?? LoadNative; - /// public string Id => ProfileId; /// public IImageParser? CreateImageParser() => null; - - /// - public IPdfParser? CreatePdfParser() => new NativePdfParser(_opener); - - /// - public void Prepare() => _prepare(); - - private static PdfDocument OpenNative(IPdfInput input, PdfLimits limits, CancellationToken cancellation) => - PdfDocument.Open(input, limits, default, cancellation); - - /// Checks what the loaded library reports: the functional ABI (minor 1 or later) and a linked PDF backend. - internal static void VerifyLibrary(NativeAbiVersion version, string buildInfo) - { - ArgumentNullException.ThrowIfNull(buildInfo); - if (version.Major != 1 || version.Minor < 1) - { - throw new ContentParserException("The PDF library does not export the functional ABI."); - } - - if (!buildInfo.Contains("backend=linked", StringComparison.Ordinal)) - { - throw new ContentParserException("The PDF library has no parser linked."); - } - } - - [SuppressMessage("Design", "CA1031", Justification = "Any load failure is the same fail-closed refusal.")] - private static void LoadNative() - { - try - { - VerifyLibrary(PdfAbi.GetAbiVersion(), PdfAbi.GetBuildInfo()); - } - catch (ContentParserException) - { - throw; - } - catch (Exception exception) when (exception is not OutOfMemoryException) - { - // Every way the library can fail to load (missing file, wrong manifest or hash, wrong bitness, platform, type initialisation) - // is the same refusal: the helper ends before any parser exists. - throw new ContentParserException("The PDF library could not be loaded.", exception); - } - } } diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Program.cs b/src/DesktopHelpers/ArcForges.ContentSandbox/Program.cs index 25819ad3..d370c7e8 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Program.cs +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/Program.cs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-only using ArcForges.ContentSandbox.Host; -// The production helper composes the approved production parsers (the PDF parser over the native library); a launch that names a -// composition this build does not contain, including the hostile test composition, is refused. +// The production helper composes the approved production parsers; the PDF parser path is retired (P2-022) and the still-image composition +// is not yet delivered. A launch that names a composition this build does not contain, including the hostile test composition, is refused. return await HelperEntry.RunAsync(ParserProfiles.Production).ConfigureAwait(false); diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/README.md b/src/DesktopHelpers/ArcForges.ContentSandbox/README.md index 4104ddd6..ea19c44f 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/README.md +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/README.md @@ -1,6 +1,6 @@ # ArcForges.ContentSandbox -The first-party Native AOT content helper host (WP-11.09, PLT.45). A product never parses hostile PDF or image bytes in its own process: +The first-party Native AOT content helper host (WP-11.09, PLT.45). A product never parses hostile image bytes in its own process: it launches this helper in a restricted operating-system profile through `ArcForges.ContentSandbox.Broker` and talks to it over the generated `ContentSandboxService` contract. This project is non-packable; package admission is PLT.46. @@ -15,7 +15,7 @@ generated `ContentSandboxService` contract. This project is non-packable; packag keeps its registration lease, and leaves the process when the parent is lost, the session lease passes or the session is closed. Renewal and cancellation are declared reserved control methods, so they pass a full data lane. It listens on no network endpoint. - **Service (`Host/ContentSandboxServiceImpl`).** Holds exactly one invocation-scoped session whose identity, input and budget the launch fixed. - It chooses parsers only from the composition named by the launch (the production helper composes `ProductionParserProfile`: the PDF parser over the native `arc_pdf_*` library; no image parser is composed yet), fills only the slot grants the parent made, seals exactly the bytes written, and checks every + It chooses parsers only from the composition named by the launch (the production helper composes `ProductionParserProfile`, which composes no parser yet: the local PDF path is retired by P2-022 and the still-image composition is NAT.31), fills only the slot grants the parent made, seals exactly the bytes written, and checks every result with the same shape rules the parent applies again. A parser that fails, overruns its deadline or is cancelled ends the invocation. - **Profiles.** Windows: the parent creates the process in an AppContainer and Job Object; the helper checks it cannot reach the parent process or the user profile. Linux (`Native/LinuxEnforcement`): the helper applies no_new_privs, resource limits, Landlock and a seccomp @@ -31,7 +31,7 @@ generated `ContentSandboxService` contract. This project is non-packable; packag ## Tests -`Tests/` (`ArcForges.ContentSandbox.Tests`) runs offline in CI: the launch frame, the record mapping, the budget, paging of PDF text, the parent +`Tests/` (`ArcForges.ContentSandbox.Tests`) runs offline in CI: the launch frame, the record mapping, the budget, the retired PDF calls answering their closed refusals without a parser, the parent and the real helper host end to end over in-memory streams (grant, seal, private copy and digest on the copy, acknowledgement, cancellation through the control slot while the data lane is full, deadlines), a helper that lies, the pure parts of the Linux profile (a classic-BPF interpreter runs the seccomp program), and the macOS refusal. **The in-process helper is not operating-system containment.** @@ -54,19 +54,14 @@ See the Plan ledger record of PLT.45. In short: the Linux profile and launcher a on Linux. macOS has no launcher. Windows signature (Authenticode) verification of the helper is not implemented: the launcher pins the SHA-256 of the installed helper from the signed inventory. -## Production composition (NAT.14) +## Production composition -`ProductionParserProfile` (`arcforges-parsers-v1`) is the one composition the production helper contains. Its PDF parser -(`Host/NativePdfParser`) adapts the sandbox parser interface to the native `arc_pdf_*` library through `ArcForges.Native.Pdf`: -it passes the launch budget on, assembles page text from bounded native chunks, validates every geometry, box and size it takes -from the native side, and turns every native or loader failure into one parser failure. `Prepare` runs before the operating-system -profile is applied (a restricted process cannot load a library afterwards) and ends the helper when the library cannot be loaded or -has no PDF backend linked (`backend=none`); the helper exits with the internal-failure code and writes one diagnostic line. The hostile -test composition is never registered in production; the fixture stays a test-only regression executable. +`ProductionParserProfile` (`arcforges-parsers-v1`) is the one composition the production helper contains. It composes no parser: +the local PDF parser path (NAT.14, its native `arc_pdf_*` engine and `ArcForges.Native.Pdf` binding) is retired by P2-022 and +removed by NAT.32, and the still-image parser composition is owned by NAT.31. The five ContentSandbox PDF RPCs stay in the published +`arcforges.local.sandbox.v1` schema unchanged. The service answers them without reaching any parser: `OpenPdf` answers +`resource.parser_failed`, a malformed call answers `validation.invalid_request`, the other read calls answer `state.not_found`, and +`ClosePdf` returns its idempotent receipt. The hostile test composition is never registered in production. -**What is not proven.** `native/arcpdf-abi` links no PDF parser yet, so a production helper built from this tree refuses every PDF -(the library reports `backend=none`). No real PDF was parsed, no operating-system isolation check was re-run against a real parser, -the ACL a Windows AppContainer needs on the directory that holds the native libraries is unobserved, and Linux and macOS have no -native library path (`NativeLoader` is win-x64 only). The offline tests drive the adapter with a scripted native document and the -engine with a scripted backend; they prove the containment and limit code, not PDFium. Real-parser composition and acceptance are -tracked as NAT.15. +**What is not proven.** The PDF path is retired, not completed: no PDF parsing, rendering, preview or AI PDF reading is claimed. Still-image +composition and its real-parser containment evidence are NAT.31 and PLT.54. diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/InvocationTests.cs b/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/InvocationTests.cs index 0cc03be5..a646312f 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/InvocationTests.cs +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/InvocationTests.cs @@ -120,60 +120,122 @@ public async Task AScriptTheParserCannotReadFailsTheCallAndKeepsTheInvocation() } [Fact] - public async Task APdfIsOpenedPagedAndExtractedInBoundedChunksAndRendered() + public async Task OpenPdfIsRetiredAndAnswersItsClosedRefusalWithoutAParser() { - var (launcher, _, invocation) = await Fixtures.LaunchAsync(Fixtures.Script("page 612 792 first page of the document", "longpage 90000")); + var (launcher, _, invocation) = await Fixtures.LaunchAsync(Fixtures.Script("page 612 792 first page of the document")); await using var _l = launcher; await using var _i = invocation; - var document = await invocation.OpenPdfAsync(Ct); - Assert.True(document.IsSuccess, document.Failure?.Code); - var first = await invocation.GetPdfPageAsync(document.Value, 0, Ct); - Assert.Equal((612d, 792d), (first.Value!.WidthPoints, first.Value.HeightPoints)); - Assert.Equal("state.not_found", (await invocation.GetPdfPageAsync(document.Value, 2, Ct)).Failure!.Code); - - var small = await invocation.ExtractPdfTextAsync(document.Value, 0, 0, Ct); - Assert.Equal("first page of the document", small.Value!.Text); - Assert.False(small.Value.HasNext); - Assert.Equal(5, small.Value.Boxes.Count); - - var total = 0; - uint start = 0; - while (true) - { - var chunk = (await invocation.ExtractPdfTextAsync(document.Value, 1, start, Ct)).Value!; - Assert.NotNull(chunk); - total += chunk.Text.Length; - if (!chunk.HasNext) + var refused = await invocation.RawClient.OpenPdfAsync( + new ContentSandboxServiceOpenPdfRequest { - break; - } + Meta = Meta(), + SessionId = SandboxRecords.ToWireId(invocation.SessionId), + DocumentId = SandboxRecords.ToWireId(Guid.NewGuid()), + }, + new CallOptions(cancellationToken: Ct)); + Assert.Equal(ContentSandboxServiceOpenPdfResponse.OutcomeOneofCase.Error, refused.OutcomeCase); + Assert.Equal("resource.parser_failed", refused.Error.Code); + var malformed = await invocation.RawClient.OpenPdfAsync( + new ContentSandboxServiceOpenPdfRequest { Meta = Meta(), DocumentId = SandboxRecords.ToWireId(Guid.NewGuid()) }, + new CallOptions(cancellationToken: Ct)); + Assert.Equal("validation.invalid_request", malformed.Error.Code); + Assert.False(invocation.IsEnded); + } - start = chunk.Next; - } + [Fact] + public async Task GetPdfPageIsRetiredAndAnswersItsClosedRefusalWithoutAParser() + { + var (launcher, _, invocation) = await Fixtures.LaunchAsync(Fixtures.Script("page 612 792 first page of the document")); + await using var _l = launcher; + await using var _i = invocation; + var refused = await invocation.RawClient.GetPdfPageAsync( + new ContentSandboxServiceGetPdfPageRequest + { + Meta = Meta(), + SessionId = SandboxRecords.ToWireId(invocation.SessionId), + DocumentId = SandboxRecords.ToWireId(Guid.NewGuid()), + PageIndex = 0, + }, + new CallOptions(cancellationToken: Ct)); + Assert.Equal("state.not_found", refused.Error.Code); + var malformed = await invocation.RawClient.GetPdfPageAsync( + new ContentSandboxServiceGetPdfPageRequest { Meta = Meta(), DocumentId = SandboxRecords.ToWireId(Guid.NewGuid()) }, + new CallOptions(cancellationToken: Ct)); + Assert.Equal("validation.invalid_request", malformed.Error.Code); + Assert.False(invocation.IsEnded); + } - Assert.Equal(90000, total); - Assert.Equal("validation.invalid_offset", (await invocation.ExtractPdfTextAsync(document.Value, 1, 7, Ct)).Failure!.Code); + [Fact] + public async Task ExtractPdfTextIsRetiredAndAnswersItsClosedRefusalWithoutAParser() + { + var (launcher, _, invocation) = await Fixtures.LaunchAsync(Fixtures.Script("page 612 792 first page of the document")); + await using var _l = launcher; + await using var _i = invocation; + var refused = await invocation.RawClient.ExtractPdfTextAsync( + new ContentSandboxServiceExtractPdfTextRequest + { + Meta = Meta(), + SessionId = SandboxRecords.ToWireId(invocation.SessionId), + DocumentId = SandboxRecords.ToWireId(Guid.NewGuid()), + PageIndex = 0, + Start = 0, + }, + new CallOptions(cancellationToken: Ct)); + Assert.Equal("state.not_found", refused.Error.Code); + var malformed = await invocation.RawClient.ExtractPdfTextAsync( + new ContentSandboxServiceExtractPdfTextRequest { Meta = Meta(), DocumentId = SandboxRecords.ToWireId(Guid.NewGuid()) }, + new CallOptions(cancellationToken: Ct)); + Assert.Equal("validation.invalid_request", malformed.Error.Code); + Assert.False(invocation.IsEnded); + } - using var tile = (await invocation.RenderPdfTileAsync(document.Value, first.Value, 600, 780, 8, 8, 32, 16, Ct)).Value!; - Assert.Equal((32u, 16u), (tile.Width, tile.Height)); - AssertPattern(tile); - Assert.True((await invocation.ClosePdfAsync(document.Value, Ct)).IsSuccess); - Assert.Equal("state.not_found", (await invocation.ClosePdfAsync(document.Value, Ct)).Failure!.Code); + [Fact] + public async Task RenderPdfTileIsRetiredAndAnswersItsClosedRefusalWithoutAParser() + { + var (launcher, _, invocation) = await Fixtures.LaunchAsync(Fixtures.Script("page 612 792 first page of the document")); + await using var _l = launcher; + await using var _i = invocation; + var refused = await invocation.RawClient.RenderPdfTileAsync( + new ContentSandboxServiceRenderPdfTileRequest + { + Meta = Meta(), + SessionId = SandboxRecords.ToWireId(invocation.SessionId), + DocumentId = SandboxRecords.ToWireId(Guid.NewGuid()), + Page = new SandboxPdfPage { PageIndex = 0, Rotation = 0, WidthPoints = 612, HeightPoints = 792 }, + Region = new SandboxRegion { X = 0, Y = 0, Width = 8, Height = 8, FirstSample = 0, SampleCount = 0, RowStride = 32 }, + Grant = new SandboxSlotGrant { SlotId = 0, Sequence = 1, Capacity = 4096 }, + FullWidth = 600, + FullHeight = 780, + }, + new CallOptions(cancellationToken: Ct)); + Assert.Equal("state.not_found", refused.Error.Code); + var malformed = await invocation.RawClient.RenderPdfTileAsync( + new ContentSandboxServiceRenderPdfTileRequest { Meta = Meta(), DocumentId = SandboxRecords.ToWireId(Guid.NewGuid()) }, + new CallOptions(cancellationToken: Ct)); + Assert.Equal("validation.invalid_request", malformed.Error.Code); + Assert.False(invocation.IsEnded); } [Fact] - public async Task APdfRenderMustUseTheGeometryThePageCallReturned() + public async Task ClosePdfIsRetiredAndReturnsItsIdempotentReceiptWithoutAParser() { - var (launcher, _, invocation) = await Fixtures.LaunchAsync(Fixtures.Script("page 612 792 hello")); + var (launcher, _, invocation) = await Fixtures.LaunchAsync(Fixtures.Script("page 612 792 first page of the document")); await using var _l = launcher; await using var _i = invocation; - var document = (await invocation.OpenPdfAsync(Ct)).Value; - var page = (await invocation.GetPdfPageAsync(document, 0, Ct)).Value!; - var scaled = page.Clone(); - scaled.WidthPoints = 100; - var refused = await invocation.RenderPdfTileAsync(document, scaled, 600, 780, 0, 0, 8, 8, Ct); - Assert.False(refused.IsSuccess); - Assert.Equal("validation.invalid_request", refused.Failure!.Code); + var closed = await invocation.RawClient.ClosePdfAsync( + new ContentSandboxServiceClosePdfRequest + { + Meta = Meta(), + SessionId = SandboxRecords.ToWireId(invocation.SessionId), + DocumentId = SandboxRecords.ToWireId(Guid.NewGuid()), + }, + new CallOptions(cancellationToken: Ct)); + Assert.Equal(ContentSandboxServiceClosePdfResponse.OutcomeOneofCase.Value, closed.OutcomeCase); + var malformed = await invocation.RawClient.ClosePdfAsync( + new ContentSandboxServiceClosePdfRequest { Meta = Meta(), DocumentId = SandboxRecords.ToWireId(Guid.NewGuid()) }, + new CallOptions(cancellationToken: Ct)); + Assert.Equal("validation.invalid_request", malformed.Error.Code); + Assert.False(invocation.IsEnded); } [Fact] @@ -214,12 +276,15 @@ public async Task CancellationReachesTheHelperThroughTheControlSlotWhileTheDataL await using var _i = invocation; // 80 calls that the helper holds: 16 run (one in the parser, the rest waiting for it) and 64 queue. The data lane is full. - var held = Enumerable.Range(0, 80).Select(_ => invocation.RawClient.OpenPdfAsync( - new ContentSandboxServiceOpenPdfRequest + var held = Enumerable.Range(0, 80).Select(_ => invocation.RawClient.OpenImageAsync( + new ContentSandboxServiceOpenImageRequest { Meta = Meta(), SessionId = SandboxRecords.ToWireId(invocation.SessionId), - DocumentId = SandboxRecords.ToWireId(Guid.NewGuid()), + ImageId = SandboxRecords.ToWireId(Guid.NewGuid()), + Subimage = 0, + Mip = 0, + OutputFormat = 1, }, new CallOptions(deadline: DateTime.UtcNow + TimeSpan.FromSeconds(28), cancellationToken: Ct)).ResponseAsync).ToArray(); await Task.Delay(TimeSpan.FromSeconds(1), Ct); diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/PdfTextPagerTests.cs b/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/PdfTextPagerTests.cs deleted file mode 100644 index e3f2242f..00000000 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/PdfTextPagerTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -using System.Text; -using ArcForges.ContentSandbox.Host; -using ArcForges.Contracts.LocalRpc.Sandbox.V1; -using Xunit; -using Check = ArcForges.Contracts.LocalRpc.Sandbox.SandboxProfile; - -namespace ArcForges.ContentSandbox.Tests; - -/// Pagination of page text into bounded chunks: UTF-16 positions, surrogate pairs, box coverage, progress and the response bound. -public sealed class PdfTextPagerTests -{ - private static PdfPageText Page(string text, params (uint Start, uint Length)[] boxes) - { - var list = boxes.Select(box => new SandboxTextBox { Start = box.Start, Length = box.Length, X = 0, Y = 0, Width = 1, Height = 1 }).ToList(); - return new PdfPageText(text, list); - } - - private static string Emoji(int count) => string.Concat(Enumerable.Repeat(char.ConvertFromUtf32(0x1F600), count)); - - [Fact] - public void ASmallPageIsOneChunkWithoutANextPosition() - { - var chunk = PdfTextPager.Cut(0, 0, Page("hello world", (0, 5), (6, 5)), 1024, 60 * 1024)!; - Assert.Equal("hello world", chunk.Text); - Assert.False(chunk.HasNext); - Assert.Equal(2, chunk.Boxes.Count); - Assert.True(Check.IsValid(chunk)); - } - - [Fact] - public void AnEmptyPageIsAnEmptyChunk() - { - var chunk = PdfTextPager.Cut(3, 0, Page(string.Empty), 1024, 60 * 1024)!; - Assert.Equal(string.Empty, chunk.Text); - Assert.False(chunk.HasNext); - Assert.Equal(3u, chunk.PageIndex); - } - - [Fact] - public void ALongPageIsPaginatedWithProgressAndNoLossAndEveryChunkIsValid() - { - var text = string.Concat(Enumerable.Range(0, 20000).Select(i => "w" + i + " ")) + Emoji(5000); - var boxes = new List<(uint, uint)>(); - var position = 0; - foreach (var word in text.Split(' ')) - { - if (word.Length > 0 && !char.IsHighSurrogate(word[0])) - { - boxes.Add(((uint)position, (uint)word.Length)); - } - - position += word.Length + 1; - } - - var page = Page(text, [.. boxes]); - var rebuilt = new StringBuilder(); - uint start = 0; - var chunks = 0; - while (true) - { - var chunk = PdfTextPager.Cut(0, start, page, 1024, 60 * 1024)!; - Assert.NotNull(chunk); - Assert.True(Check.IsValid(chunk), "chunk " + chunks); - Assert.True(chunk.CalculateSize() <= 64 * 1024); - Assert.True(chunk.Boxes.Count <= 1024); - Assert.Equal(start, chunk.Start); - _ = rebuilt.Append(chunk.Text); - chunks++; - if (!chunk.HasNext) - { - break; - } - - Assert.True(chunk.Next > start); - start = chunk.Next; - Assert.True(chunks < 100000); - } - - Assert.Equal(text, rebuilt.ToString()); - Assert.True(chunks > 3); - } - - [Fact] - public void ACutNeverSplitsASurrogatePair() - { - var text = new string('a', 16 * 1024 - 1) + Emoji(4); - var first = PdfTextPager.Cut(0, 0, Page(text), 1024, 60 * 1024)!; - Assert.True(first.HasNext); - Assert.False(char.IsLowSurrogate(text[(int)first.Next])); - Assert.False(char.IsHighSurrogate(first.Text[^1])); - Assert.True(Check.IsValid(first)); - } - - [Fact] - public void AStartInTheMiddleOfAPairOrPastTheEndIsRefused() - { - var text = "ab" + Emoji(2); - Assert.Null(PdfTextPager.Cut(0, 3, Page(text), 1024, 60 * 1024)); - Assert.Null(PdfTextPager.Cut(0, (uint)text.Length + 1, Page(text), 1024, 60 * 1024)); - Assert.NotNull(PdfTextPager.Cut(0, 2, Page(text), 1024, 60 * 1024)); - } - - [Fact] - public void OnlyBoxesWhollyInsideTheChunkAreCarried() - { - var text = new string('a', 20000); - var chunk = PdfTextPager.Cut(0, 0, Page(text, (0, 10), (16380, 20), (19000, 10)), 1024, 60 * 1024)!; - Assert.True(chunk.HasNext); - Assert.All(chunk.Boxes, box => Assert.True(box.Start + box.Length <= chunk.Next)); - Assert.True(Check.IsValid(chunk)); - } - - [Fact] - public void TheBoxBoundCutsTheChunkAndStillMakesProgress() - { - var text = string.Concat(Enumerable.Repeat("ab ", 100)); - var boxes = Enumerable.Range(0, 100).Select(i => ((uint)(i * 3), 2u)).ToArray(); - var chunk = PdfTextPager.Cut(0, 0, Page(text, boxes), 10, 60 * 1024)!; - Assert.Equal(10, chunk.Boxes.Count); - Assert.True(chunk.HasNext); - Assert.True(chunk.Next > 0); - Assert.Null(PdfTextPager.Cut(0, 0, Page("ab", (0, 1), (0, 1)), 0, 60 * 1024)); - } - - [Fact] - public void AByteBoundTooSmallForOneCharacterRefusesInsteadOfLooping() => - Assert.Null(PdfTextPager.Cut(0, 0, Page("abc"), 1024, 4)); -} diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ProductionPdfCompositionTests.cs b/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ProductionPdfCompositionTests.cs deleted file mode 100644 index 9dd7df1c..00000000 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/ProductionPdfCompositionTests.cs +++ /dev/null @@ -1,579 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -using System.Text; -using ArcForges.ContentSandbox.Broker; -using ArcForges.ContentSandbox.Contracts; -using ArcForges.ContentSandbox.Host; -using ArcForges.Contracts.LocalRpc.Sandbox.V1; -using ArcForges.Native.Abstractions; -using ArcForges.Native.Pdf; -using Xunit; - -namespace ArcForges.ContentSandbox.Tests; - -/// What a scripted native document does when it is asked for text or a tile. -internal enum ScriptedRender -{ - Normal, - ThrowNative, - SpinUntilCancelled, - HangIgnoringCancellation, - ReportsMoreThanItWrote, - ReportsMoreThanTheDestination, - FailsAfterWriting, -} - -/// -/// TEST ONLY. A scripted stand-in for the native PDF document, below the production adapter (). It parses no -/// real PDF; it exists so the adapter's limits, validation, paging, cancellation and failure mapping can be driven with hostile answers. -/// -internal sealed class ScriptedPdfDocument : IPdfDocument -{ - internal List<(PdfPageGeometry Geometry, string Text, PdfTextBox[] Boxes)> Pages { get; } = []; - - internal ScriptedRender RenderBehaviour { get; set; } - - internal Func? TextOverride { get; set; } - - internal int RenderCalls { get; private set; } - - internal int TextCalls { get; private set; } - - internal int Disposed { get; private set; } - - internal (uint X, uint Y, uint W, uint H, uint FullW, uint FullH, uint Stride)? LastTile { get; private set; } - - internal ManualResetEventSlim Release { get; } = new(false); - - public uint PageCount => (uint)Pages.Count; - - internal static ScriptedPdfDocument OnePage(string text = "hello world", params PdfTextBox[] boxes) - { - var document = new ScriptedPdfDocument(); - document.Pages.Add((new PdfPageGeometry(0, 0, 612, 792), text, boxes)); - return document; - } - - public PdfPageGeometry GetPage(uint pageIndex) - { - if (pageIndex >= Pages.Count) - { - throw new PdfNativeException(NativeStatus.NotFound, "no such page"); - } - - return Pages[(int)pageIndex].Geometry; - } - - public PdfTextChunk ExtractText(uint pageIndex, uint start, uint count, CancellationToken cancellation) - { - TextCalls++; - cancellation.ThrowIfCancellationRequested(); - if (TextOverride?.Invoke(pageIndex, start) is { } forced) - { - return forced; - } - - var page = Pages[(int)pageIndex]; - var end = (int)Math.Min(page.Text.Length, (long)start + count); - if (end < page.Text.Length && end > start && char.IsLowSurrogate(page.Text[end]) && char.IsHighSurrogate(page.Text[end - 1])) - { - end--; - } - - var boxes = page.Boxes.Where(box => box.Start >= start && (ulong)box.Start + box.Length <= (ulong)end).ToList(); - return new PdfTextChunk(pageIndex, start, end < page.Text.Length ? (uint)end : null, page.Text[(int)start..end], boxes); - } - - public int Render(PdfPageGeometry page, uint x, uint y, uint width, uint height, uint fullWidth, uint fullHeight, uint rowStride, Span destination, CancellationToken cancellation) - { - RenderCalls++; - LastTile = (x, y, width, height, fullWidth, fullHeight, rowStride); - switch (RenderBehaviour) - { - case ScriptedRender.ThrowNative: - throw new PdfNativeException(NativeStatus.Corrupt, "the page is corrupt"); - case ScriptedRender.SpinUntilCancelled: - while (true) - { - cancellation.ThrowIfCancellationRequested(); - Thread.Sleep(1); - } - - case ScriptedRender.HangIgnoringCancellation: - _ = Release.Wait(TimeSpan.FromSeconds(60), CancellationToken.None); - return 0; - } - - var required = ((int)(height - 1) * (int)rowStride) + ((int)width * 4); - for (var row = 0; row < height; row++) - { - for (var column = 0; column < width; column++) - { - var offset = (row * (int)rowStride) + (column * 4); - destination[offset] = (byte)((x + column) & 255); - destination[offset + 1] = (byte)((y + row) & 255); - destination[offset + 2] = (byte)(page.PageIndex & 255); - destination[offset + 3] = 255; - } - } - - return RenderBehaviour switch - { - ScriptedRender.ReportsMoreThanItWrote => required + 64, - ScriptedRender.ReportsMoreThanTheDestination => destination.Length + 1, - ScriptedRender.FailsAfterWriting => throw new PdfNativeException(NativeStatus.Internal, "failed after writing"), - _ => required, - }; - } - - public void Dispose() => Disposed++; -} - -/// The strict reader of the native text encoding. -public sealed class PdfTextJsonTests -{ - private static PdfTextChunk Parse(string json, uint page = 0, uint start = 0) => PdfTextJson.Parse(Encoding.UTF8.GetBytes(json), page, start); - - private const string Golden = "{\"version\":1,\"page\":0,\"start\":0,\"next\":5,\"text\":\"hello\",\"boxes\":[{\"start\":0,\"length\":5,\"x\":1,\"y\":2,\"width\":3,\"height\":4}]}"; - - [Fact] - public void TheEncodingTheNativeEngineWritesIsRead() - { - var chunk = Parse(Golden); - Assert.Equal("hello", chunk.Text); - Assert.Equal(5u, chunk.Next); - Assert.Equal(new PdfTextBox(0, 5, 1, 2, 3, 4), Assert.Single(chunk.Boxes)); - - var last = Parse("{\"version\":1,\"page\":2,\"start\":5,\"text\":\"\\ud83d\\ude00\\u000a\",\"boxes\":[]}", 2, 5); - Assert.Equal("\U0001F600\n", last.Text); - Assert.Null(last.Next); - } - - [Theory] - [InlineData("{\"version\":2,\"page\":0,\"start\":0,\"text\":\"\",\"boxes\":[]}")] - [InlineData("{\"version\":1,\"page\":1,\"start\":0,\"text\":\"\",\"boxes\":[]}")] - [InlineData("{\"version\":1,\"page\":0,\"start\":3,\"text\":\"\",\"boxes\":[]}")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"boxes\":[]}")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"text\":\"\"}")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"text\":\"\",\"boxes\":[],\"extra\":1}")] - [InlineData("{\"version\":1,\"version\":1,\"page\":0,\"start\":0,\"text\":\"\",\"boxes\":[]}")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"next\":0,\"text\":\"ab\",\"boxes\":[]}")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"next\":9,\"text\":\"ab\",\"boxes\":[]}")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"text\":\"ab\",\"boxes\":[{\"start\":1,\"length\":5,\"x\":0,\"y\":0,\"width\":1,\"height\":1}]}")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"text\":\"ab\",\"boxes\":[{\"start\":0,\"length\":0,\"x\":0,\"y\":0,\"width\":1,\"height\":1}]}")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"text\":\"ab\",\"boxes\":[{\"start\":0,\"length\":1,\"x\":0,\"y\":0,\"width\":-1,\"height\":1}]}")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"text\":\"ab\",\"boxes\":[{\"start\":0,\"length\":1,\"x\":1e999,\"y\":0,\"width\":1,\"height\":1}]}")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"text\":\"ab\",\"boxes\":[{\"start\":0,\"length\":1,\"x\":0,\"y\":0,\"width\":1}]}")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"text\":7,\"boxes\":[]}")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"text\":\"\",\"boxes\":{}}")] - [InlineData("[]")] - [InlineData("")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"text\":\"\",\"boxes\":[]} trailing")] - [InlineData("{\"version\":1,\"page\":0,\"start\":0,\"text\":\"\",\"boxes\":[],}")] - public void AnythingElseRefusesTheWholeChunk(string json) => - _ = Assert.Throws(() => Parse(json)); - - [Fact] - public void ABoxBeforeTheChunkIsRefused() => - _ = Assert.Throws(() => Parse("{\"version\":1,\"page\":0,\"start\":4,\"text\":\"ab\",\"boxes\":[{\"start\":3,\"length\":1,\"x\":0,\"y\":0,\"width\":1,\"height\":1}]}", 0, 4)); -} - -/// The production PDF adapter driven with hostile answers of the native side. -public sealed class NativePdfParserTests -{ - private static readonly ContentSandboxLimits Limits = new() { TimeoutMs = 5000, MaxWidth = 4096, MaxHeight = 4096 }; - - private static ParserContext Context() => new(Limits, Xunit.TestContext.Current.CancellationToken); - - private static ParserContext ContextWith(CancellationToken cancelled) => new(Limits, cancelled); - - private static ParserInput Input() => new(new Mapping(new byte[16]), 16); - - private static NativePdfParser Parser(ScriptedPdfDocument document, Action? seen = null) => - new((_, limits, _) => - { - seen?.Invoke(limits); - return document; - }); - - private sealed class Mapping(byte[] bytes) : ArcForges.LocalRpc.ILocalRpcBufferMapping - { - public long Length => bytes.Length; - - public int Read(long offset, Span destination) - { - var count = (int)Math.Min(destination.Length, bytes.Length - offset); - bytes.AsSpan((int)offset, count).CopyTo(destination); - return count; - } - - public void Dispose() - { - } - } - - [Fact] - public void OpenPassesTheLaunchBudgetToTheNativeLibraryAndReturnsThePageCount() - { - PdfLimits? seen = null; - using var parser = Parser(ScriptedPdfDocument.OnePage(), limits => seen = limits); - Assert.Equal(1u, parser.Open(Input(), Context())); - Assert.Equal(new PdfLimits(Limits.MaxInputBytes, Limits.MaxMemoryBytes, Limits.MaxOutputBytes, 4096, 4096, Limits.MaxItems, 5000), seen); - _ = Assert.Throws(() => parser.Open(Input(), Context())); - } - - [Fact] - public void ADocumentWithoutPagesIsRefusedAndReleased() - { - var empty = new ScriptedPdfDocument(); - using var parser = Parser(empty); - _ = Assert.Throws(() => parser.Open(Input(), Context())); - Assert.Equal(1, empty.Disposed); - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(5)] - [InlineData(6)] - [InlineData(7)] - public void EveryNativeOrLoaderFailureIsOneParserFailure(int which) - { - Exception failure = which switch - { - 0 => new PdfNativeException(NativeStatus.Corrupt, "bad"), - 1 => new DllNotFoundException(), - 2 => new EntryPointNotFoundException(), - 3 => new BadImageFormatException(), - 4 => new PlatformNotSupportedException(), - 5 => new InvalidDataException(), - 6 => new FileNotFoundException(), - _ => new InvalidOperationException(), - }; - using var parser = new NativePdfParser((_, _, _) => throw failure); - _ = Assert.Throws(() => parser.Open(Input(), Context())); - } - - [Fact] - public void ACancellationIsNotAParserFailure() - { - using var source = new CancellationTokenSource(); - using var parser = new NativePdfParser((_, _, token) => - { - source.Cancel(); - token.ThrowIfCancellationRequested(); - return ScriptedPdfDocument.OnePage(); - }); - _ = Assert.ThrowsAny(() => parser.Open(Input(), ContextWith(source.Token))); - } - - [Theory] - [InlineData(double.NaN, 792.0, 0u)] - [InlineData(612.0, double.PositiveInfinity, 0u)] - [InlineData(0.0, 792.0, 0u)] - [InlineData(612.0, -1.0, 0u)] - [InlineData(612.0, 792.0, 45u)] - public void APageWithImpossibleGeometryIsRefused(double width, double height, uint rotation) - { - var document = new ScriptedPdfDocument(); - document.Pages.Add((new PdfPageGeometry(0, rotation, width, height), "x", [])); - using var parser = Parser(document); - _ = parser.Open(Input(), Context()); - _ = Assert.Throws(() => parser.GetPage(0, Context())); - } - - [Fact] - public void APageTheNativeLibraryDoesNotHaveIsAParserFailure() - { - using var parser = Parser(ScriptedPdfDocument.OnePage()); - _ = parser.Open(Input(), Context()); - var page = parser.GetPage(0, Context()); - Assert.Equal((0u, 0u, 612d, 792d), (page.PageIndex, page.Rotation, page.WidthPoints, page.HeightPoints)); - _ = Assert.Throws(() => parser.GetPage(5, Context())); - } - - [Fact] - public void LongPageTextIsAssembledFromBoundedNativeChunksWithAbsoluteBoxes() - { - var text = string.Concat(Enumerable.Repeat("word \U0001F600 ", 30000)); // 240000 UTF-16 units: four native chunks - var boxes = new List(); - for (uint position = 0; position + 4 <= text.Length; position += 9) - { - boxes.Add(new PdfTextBox(position, 4, position, 0, 4, 10)); - } - - var document = ScriptedPdfDocument.OnePage(text, [.. boxes]); - using var parser = Parser(document); - _ = parser.Open(Input(), Context()); - var page = parser.GetPageText(0, Context()); - Assert.Equal(text, page.Text); - Assert.True(document.TextCalls >= 4); - // A box that crosses a chunk end is not carried by either chunk (the native rule); every other box arrives once, at its absolute offset. - var expected = boxes.Select(box => (box.Start, box.Length)).ToHashSet(); - Assert.All(page.Boxes, box => Assert.Contains((box.Start, box.Length), expected)); - Assert.InRange(page.Boxes.Count, boxes.Count - document.TextCalls, boxes.Count); - Assert.Equal(page.Boxes.Count, page.Boxes.Select(box => box.Start).Distinct().Count()); - } - - [Fact] - public void ATextChunkThatMakesNoProgressOrAnswersAnotherPositionIsRefused() - { - var document = ScriptedPdfDocument.OnePage("abcdef"); - document.TextOverride = (page, start) => start == 0 ? new PdfTextChunk(page, 0, 3, "abc", []) : new PdfTextChunk(page, 3, 3, "def", []); - using var parser = Parser(document); - _ = parser.Open(Input(), Context()); - _ = Assert.Throws(() => parser.GetPageText(0, Context())); - - document.TextOverride = (page, start) => new PdfTextChunk(page, start + 1, null, "x", []); - _ = Assert.Throws(() => parser.GetPageText(0, Context())); - - document.TextOverride = (page, start) => new PdfTextChunk(page, start, start + 10, "abc", []); - _ = Assert.Throws(() => parser.GetPageText(0, Context())); - } - - [Fact] - public void ATextVolumeOrBoxCountBeyondTheBoundIsRefused() - { - var huge = new string('x', 65536); - var document = ScriptedPdfDocument.OnePage("a"); - document.TextOverride = (page, start) => new PdfTextChunk(page, start, start + (uint)huge.Length, huge, []); - using var parser = Parser(document); - _ = parser.Open(Input(), Context()); - _ = Assert.Throws(() => parser.GetPageText(0, Context())); - - var boxes = Enumerable.Range(0, 500_000).Select(_ => new PdfTextBox(0, 1, 0, 0, 1, 1)).ToList(); - var crowded = ScriptedPdfDocument.OnePage("a"); - var round = 0; - crowded.TextOverride = (page, start) => ++round > 3 ? throw new InvalidOperationException("must have stopped") : new PdfTextChunk(page, start, start + 1, "a", boxes); - using var crowdedParser = Parser(crowded); - _ = crowdedParser.Open(Input(), Context()); - _ = Assert.Throws(() => crowdedParser.GetPageText(0, Context())); - } - - [Fact] - public void TextExtractionStopsBetweenChunksWhenCancelled() - { - using var source = new CancellationTokenSource(); - var document = ScriptedPdfDocument.OnePage(new string('y', 200000)); - using var parser = Parser(document); - _ = parser.Open(Input(), ContextWith(source.Token)); - document.TextOverride = (page, start) => - { - source.Cancel(); - return new PdfTextChunk(page, start, start + 1, "y", []); - }; - _ = Assert.ThrowsAny(() => parser.GetPageText(0, ContextWith(source.Token))); - Assert.Equal(1, document.TextCalls); - } - - [Fact] - public void ATileIsRenderedWithTheRegionTheStrideAndTheGridOfTheRequest() - { - var document = ScriptedPdfDocument.OnePage(); - using var parser = Parser(document); - _ = parser.Open(Input(), Context()); - var page = parser.GetPage(0, Context()); - var region = new SandboxRegion { X = 8, Y = 4, Width = 3, Height = 2, RowStride = 20 }; - var destination = new byte[4096]; - var written = parser.RenderTile(page, region, 100, 200, destination, Context()); - Assert.Equal(32, written); - Assert.Equal((8u, 4u, 3u, 2u, 100u, 200u, 20u), document.LastTile); - Assert.Equal(new byte[] { 8, 4, 0, 255 }, destination[..4]); - Assert.Equal(new byte[] { 8, 5, 0, 255 }, destination[20..24]); - } - - [Theory] - [InlineData((int)ScriptedRender.ThrowNative)] - [InlineData((int)ScriptedRender.ReportsMoreThanTheDestination)] - [InlineData((int)ScriptedRender.FailsAfterWriting)] - public void ARenderThatFailsOrOverclaimsIsOneParserFailure(int behaviour) - { - var document = ScriptedPdfDocument.OnePage(); - document.RenderBehaviour = (ScriptedRender)behaviour; - using var parser = Parser(document); - _ = parser.Open(Input(), Context()); - var page = parser.GetPage(0, Context()); - var region = new SandboxRegion { X = 0, Y = 0, Width = 4, Height = 4, RowStride = 16 }; - _ = Assert.Throws(() => parser.RenderTile(page, region, 64, 64, new byte[4096], Context())); - } - - [Fact] - public void AStrideBeyondTheNativeRangeIsRefusedBeforeTheNativeCall() - { - var document = ScriptedPdfDocument.OnePage(); - using var parser = Parser(document); - _ = parser.Open(Input(), Context()); - var page = parser.GetPage(0, Context()); - var region = new SandboxRegion { X = 0, Y = 0, Width = 4, Height = 4, RowStride = (ulong)uint.MaxValue + 1 }; - _ = Assert.Throws(() => parser.RenderTile(page, region, 64, 64, new byte[4096], Context())); - Assert.Equal(0, document.RenderCalls); - } - - [Fact] - public void ARenderIsCancelledThroughTheTokenAndDisposeReleasesTheDocumentOnce() - { - using var source = new CancellationTokenSource(); - var document = ScriptedPdfDocument.OnePage(); - document.RenderBehaviour = ScriptedRender.SpinUntilCancelled; - var parser = Parser(document); - _ = parser.Open(Input(), Context()); - var page = parser.GetPage(0, Context()); - var region = new SandboxRegion { X = 0, Y = 0, Width = 4, Height = 4, RowStride = 16 }; - source.CancelAfter(50); - _ = Assert.ThrowsAny(() => parser.RenderTile(page, region, 64, 64, new byte[4096], ContextWith(source.Token))); - parser.Dispose(); - parser.Dispose(); - Assert.Equal(1, document.Disposed); - _ = Assert.Throws(() => parser.GetPage(0, Context())); - } -} - -/// The production composition: what it contains, what it refuses and how it is prepared. -public sealed class ProductionCompositionTests -{ - [Fact] - public void TheProductionHelperComposesTheProductionParsersOnlyAndNeverTheHostileTestParser() - { - var production = ParserProfiles.Production; - var composed = Assert.IsType(production.Find(ProductionParserProfile.ProfileId)); - Assert.Null(composed.CreateImageParser()); - using var pdf = composed.CreatePdfParser(); - Assert.NotNull(pdf); - Assert.Null(production.Find(HostileFixture.HostileProfile.ProfileId)); - Assert.Null(production.Find("none")); - } - - [Fact] - public void PreparationFailsClosedWhenTheNativeLibraryCannotBeLoaded() - { - // This test host has no ArcPdfNative next to it (and no manifest): the production preparation must refuse, not defer to the first call. - var profile = new ProductionParserProfile(); - _ = Assert.Throws(profile.Prepare); - } - - [Fact] - public void ALibraryWithoutAPdfBackendOrWithoutTheFunctionalAbiIsRefused() - { - var functional = new NativeAbiVersion(1, 1); - ProductionParserProfile.VerifyLibrary(functional, "ArcPdfNative;abi=1.1;backend=linked"); - _ = Assert.Throws(() => ProductionParserProfile.VerifyLibrary(functional, "ArcPdfNative;abi=1.1;backend=none")); - _ = Assert.Throws(() => ProductionParserProfile.VerifyLibrary(functional, "ArcPdfNative;abi=1.1")); - _ = Assert.Throws(() => ProductionParserProfile.VerifyLibrary(new NativeAbiVersion(1, 0), "ArcPdfNative;abi=1.0;backend=linked")); - _ = Assert.Throws(() => ProductionParserProfile.VerifyLibrary(new NativeAbiVersion(2, 1), "ArcPdfNative;abi=2.1;backend=linked")); - } - - [Fact] - public void ACompositionPreparesBeforeAnyParserExists() - { - var prepared = 0; - var profile = new ProductionParserProfile(prepare: () => prepared++); - profile.Prepare(); - Assert.Equal(1, prepared); - } - - [Fact] - public async Task APdfOpensPagesExtractsAndRendersEndToEndThroughTheRealHostAndBroker() - { - var document = ScriptedPdfDocument.OnePage("first page"); - document.Pages.Add((new PdfPageGeometry(1, 90, 300, 400), new string('z', 70000), [])); - var launcher = new InProcessHelperLauncher(new ParserProfiles([new ProductionParserProfile((_, _, _) => document, () => { })])); - var (sandbox, _, invocation) = await Fixtures.LaunchAsync(new byte[] { 1, 2, 3, 4 }, Fixtures.Options(parser: ProductionParserProfile.ProfileId), launcher); - await using var _l = sandbox; - await using var _i = invocation; - var ct = Xunit.TestContext.Current.CancellationToken; - - var opened = await invocation.OpenPdfAsync(ct); - Assert.True(opened.IsSuccess, opened.Failure?.Code); - var first = (await invocation.GetPdfPageAsync(opened.Value, 0, ct)).Value!; - Assert.Equal((612d, 792d), (first.WidthPoints, first.HeightPoints)); - var second = (await invocation.GetPdfPageAsync(opened.Value, 1, ct)).Value!; - Assert.Equal(90u, second.Rotation); - - var text = (await invocation.ExtractPdfTextAsync(opened.Value, 0, 0, ct)).Value!; - Assert.Equal("first page", text.Text); - var total = 0; - uint start = 0; - while (true) - { - var chunk = (await invocation.ExtractPdfTextAsync(opened.Value, 1, start, ct)).Value!; - total += chunk.Text.Length; - if (!chunk.HasNext) - { - break; - } - - start = chunk.Next; - } - - Assert.Equal(70000, total); - using var tile = (await invocation.RenderPdfTileAsync(opened.Value, first, 600, 780, 8, 8, 16, 8, ct)).Value!; - Assert.Equal((16u, 8u), (tile.Width, tile.Height)); - Assert.Equal((8u, 8u, 16u, 8u, 600u, 780u), (document.LastTile!.Value.X, document.LastTile.Value.Y, document.LastTile.Value.W, document.LastTile.Value.H, document.LastTile.Value.FullW, document.LastTile.Value.FullH)); - Assert.True((await invocation.ClosePdfAsync(opened.Value, ct)).IsSuccess); - Assert.Equal(1, document.Disposed); - } - - [Fact] - public async Task ARenderBeyondTheDocumentBudgetIsRefusedWithoutTheNativeLibraryBeingCalled() - { - var document = ScriptedPdfDocument.OnePage(); - var launcher = new InProcessHelperLauncher(new ParserProfiles([new ProductionParserProfile((_, _, _) => document, () => { })])); - var (sandbox, _, invocation) = await Fixtures.LaunchAsync(new byte[] { 1, 2, 3, 4 }, Fixtures.Options(parser: ProductionParserProfile.ProfileId), launcher); - await using var _l = sandbox; - await using var _i = invocation; - var ct = Xunit.TestContext.Current.CancellationToken; - var opened = (await invocation.OpenPdfAsync(ct)).Value; - var page = (await invocation.GetPdfPageAsync(opened, 0, ct)).Value!; - var tooWide = await invocation.RenderPdfTileAsync(opened, page, 5000, 100, 0, 0, 8, 8, ct); - Assert.False(tooWide.IsSuccess); - var outside = await invocation.RenderPdfTileAsync(opened, page, 100, 100, 96, 0, 8, 8, ct); - Assert.False(outside.IsSuccess); - Assert.Equal(0, document.RenderCalls); - } - - [Fact] - public async Task AnOpenThatFailsInTheNativeLibraryIsOneParserFailureAndTheParentCarriesOn() - { - var launcher = new InProcessHelperLauncher(new ParserProfiles([new ProductionParserProfile((_, _, _) => throw new PdfNativeException(NativeStatus.Corrupt, "not a pdf"), () => { })])); - var (sandbox, _, invocation) = await Fixtures.LaunchAsync(new byte[] { 1, 2, 3, 4 }, Fixtures.Options(parser: ProductionParserProfile.ProfileId), launcher); - await using var _l = sandbox; - await using var _i = invocation; - var opened = await invocation.OpenPdfAsync(Xunit.TestContext.Current.CancellationToken); - Assert.False(opened.IsSuccess); - Assert.Equal("resource.parser_failed", opened.Failure!.Code); - } - - [Fact] - public async Task AnImageAgainstTheProductionCompositionIsRefusedBecauseNoImageParserIsComposed() - { - var launcher = new InProcessHelperLauncher(new ParserProfiles([new ProductionParserProfile((_, _, _) => ScriptedPdfDocument.OnePage(), () => { })])); - var (sandbox, _, invocation) = await Fixtures.LaunchAsync(new byte[] { 1, 2, 3, 4 }, Fixtures.Options(parser: ProductionParserProfile.ProfileId), launcher); - await using var _l = sandbox; - await using var _i = invocation; - var image = await invocation.OpenImageAsync(0, 0, 1, Xunit.TestContext.Current.CancellationToken); - Assert.False(image.IsSuccess); - } - - [Fact] - public async Task ANativeRenderThatNeverReturnsEndsTheHelperAtItsDeadlineAndTheParentCarriesOn() - { - var document = ScriptedPdfDocument.OnePage(); - document.RenderBehaviour = ScriptedRender.HangIgnoringCancellation; - var limits = new ContentSandboxLimits { TimeoutMs = 600, MaxWidth = 4096, MaxHeight = 4096 }; - var launcher = new InProcessHelperLauncher(new ParserProfiles([new ProductionParserProfile((_, _, _) => document, () => { })])); - var (sandbox, _, invocation) = await Fixtures.LaunchAsync(new byte[] { 1, 2, 3, 4 }, Fixtures.Options(parser: ProductionParserProfile.ProfileId, limits: limits), launcher); - await using var _l = sandbox; - await using var _i = invocation; - var ct = Xunit.TestContext.Current.CancellationToken; - var opened = (await invocation.OpenPdfAsync(ct)).Value; - var page = (await invocation.GetPdfPageAsync(opened, 0, ct)).Value!; - var started = DateTime.UtcNow; - var tile = await invocation.RenderPdfTileAsync(opened, page, 64, 64, 0, 0, 8, 8, ct); - Assert.False(tile.IsSuccess); - Assert.True(DateTime.UtcNow - started < TimeSpan.FromSeconds(20)); - Assert.Equal(ContentSandboxContract.ExitParserHung, await invocation.WaitForExitAsync(ct)); - document.Release.Set(); - } -} diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/packages.lock.json b/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/packages.lock.json index 0a21809c..c80f4c0f 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/packages.lock.json +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/Tests/packages.lock.json @@ -233,8 +233,7 @@ "dependencies": { "ArcForges.ContentSandbox.Contracts": "[1.0.0, )", "ArcForges.Foundation": "[1.0.0, )", - "ArcForges.LocalRpc": "[1.0.0, )", - "ArcForges.Native.Pdf": "[1.0.0, )" + "ArcForges.LocalRpc": "[1.0.0, )" } }, "arcforges.contentsandbox.broker": { @@ -272,15 +271,6 @@ "Grpc.Net.Client": "[2.84.0, )" } }, - "arcforges.native.abstractions": { - "type": "Project" - }, - "arcforges.native.pdf": { - "type": "Project", - "dependencies": { - "ArcForges.Native.Abstractions": "[1.0.0, )" - } - }, "ArcForges.Contracts.Foundation": { "type": "CentralTransitive", "requested": "[1.0.0-ci.216.1, )", diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/packages.lock.json b/src/DesktopHelpers/ArcForges.ContentSandbox/packages.lock.json index bb06cd00..9130ff69 100644 --- a/src/DesktopHelpers/ArcForges.ContentSandbox/packages.lock.json +++ b/src/DesktopHelpers/ArcForges.ContentSandbox/packages.lock.json @@ -72,15 +72,6 @@ "Grpc.Net.Client": "[2.84.0, )" } }, - "arcforges.native.abstractions": { - "type": "Project" - }, - "arcforges.native.pdf": { - "type": "Project", - "dependencies": { - "ArcForges.Native.Abstractions": "[1.0.0, )" - } - }, "ArcForges.Contracts.Foundation": { "type": "CentralTransitive", "requested": "[1.0.0-ci.216.1, )", diff --git a/src/Native/ArcForges.Native.Abstractions/NativeAbi.cs b/src/Native/ArcForges.Native.Abstractions/NativeAbi.cs index 4842db37..6eb5ba72 100644 --- a/src/Native/ArcForges.Native.Abstractions/NativeAbi.cs +++ b/src/Native/ArcForges.Native.Abstractions/NativeAbi.cs @@ -8,7 +8,6 @@ using System.Text.Json; [assembly: InternalsVisibleTo("ArcForges.Native.Image")] -[assembly: InternalsVisibleTo("ArcForges.Native.Pdf")] namespace ArcForges.Native.Abstractions; @@ -213,17 +212,6 @@ internal struct NativeImageOptionsV1 internal NativeLimitsV1 Limits; } -[StructLayout(LayoutKind.Sequential, Pack = 8)] -internal struct NativePdfPageV1 -{ - internal uint StructSize; - internal uint StructVersion; - internal uint PageIndex; - internal uint Rotation; - internal double WidthPoints; - internal double HeightPoints; -} - /// /// Owns one opaque, process-local native handle token. Implementations must release the token /// through their matching native close function; tokens are never domain identifiers or wire data. @@ -267,7 +255,7 @@ protected sealed override bool ReleaseHandle() } } -/// Closed numeric keys shared by the retained image, instrument and PDF ABI families. +/// Closed numeric keys shared by the retained image and instrument ABI families. public static class NativeAbiKeys { public const uint IoRead = 1; diff --git a/src/Native/ArcForges.Native.Abstractions/README.md b/src/Native/ArcForges.Native.Abstractions/README.md index be56ef25..a20b4d39 100644 --- a/src/Native/ArcForges.Native.Abstractions/README.md +++ b/src/Native/ArcForges.Native.Abstractions/README.md @@ -1,8 +1,8 @@ # ArcForges.Native.Abstractions -Shared status, opaque handle ownership, and pack-8 layout declarations for the retained ArcForges C ABI families. The Image package provides the source-generated bindings. This package contains no native binary and does not implement image, instrument, or PDF operations. +Shared status, opaque handle ownership, and pack-8 layout declarations for the retained ArcForges C ABI families. The Image package provides the source-generated bindings. This package contains no native binary and does not implement image or instrument operations. -The existing image probe exports remain ABI 1.0. Functional image, instrument, and PDF libraries advertise ABI 1.1 when those exports are delivered. Native handles are nonzero, process-local tokens owned through `NativeSafeHandle`; they are not pointers, durable identifiers, or wire values. Calls on one handle are single-caller; cancellation may be signalled concurrently. Closing drains borrowed calls before invalidating the handle generation, while children retain their parent object until released. +The existing image probe exports remain ABI 1.0. Functional image and instrument libraries advertise ABI 1.1 when those exports are delivered. Native handles are nonzero, process-local tokens owned through `NativeSafeHandle`; they are not pointers, durable identifiers, or wire values. Calls on one handle are single-caller; cancellation may be signalled concurrently. Closing drains borrowed calls before invalidating the handle generation, while children retain their parent object until released. Pin the exact package version centrally and commit package locks. The runtime package supplies app-local DLLs and a hash manifest. Loading never searches PATH or the diff --git a/src/Native/ArcForges.Native.Pdf/ArcForges.Native.Pdf.csproj b/src/Native/ArcForges.Native.Pdf/ArcForges.Native.Pdf.csproj deleted file mode 100644 index 155342fc..00000000 --- a/src/Native/ArcForges.Native.Pdf/ArcForges.Native.Pdf.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - - net10.0 - true - false - ArcForges Pdf native ABI binding: bounded page, text and tile access behind the ContentSandbox helper. - - - - - - - - AGPL - AGPL-3.0-only - - diff --git a/src/Native/ArcForges.Native.Pdf/PdfAbi.cs b/src/Native/ArcForges.Native.Pdf/PdfAbi.cs deleted file mode 100644 index 7b01ccba..00000000 --- a/src/Native/ArcForges.Native.Pdf/PdfAbi.cs +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using ArcForges.Native.Abstractions; - -[assembly: DisableRuntimeMarshalling] -[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - -namespace ArcForges.Native.Pdf; - -/// The raw arc_pdf_* exports of ArcPdfNative and the library preamble. Use for the bounded API. -internal static unsafe partial class PdfAbi -{ - static PdfAbi() => NativeLoader.Register(typeof(PdfAbi).Assembly, "ArcPdfNative"); - - /// The ABI version the loaded library reports (a functional PDF library reports 1.1). - public static NativeAbiVersion GetAbiVersion() => NativeAbi.GetVersion(GetVersionCore); - - /// The build information; it names whether a PDF backend is linked (backend=linked) or not (backend=none). - public static string GetBuildInfo() => NativeAbi.GetBuildInfo(GetBuildInfoCore, GetErrorCore); - - /// The calling thread's last native error. - public static NativeError GetLastError() => NativeAbi.GetError(GetErrorCore); - - [LibraryImport("ArcPdfNative", EntryPoint = "arc_pdf_get_abi_version")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - private static partial int GetVersionCore(uint* major, uint* minor); - - [LibraryImport("ArcPdfNative", EntryPoint = "arc_pdf_get_build_info")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - private static partial int GetBuildInfoCore(ref NativeBuffer output); - - [LibraryImport("ArcPdfNative", EntryPoint = "arc_pdf_get_last_error")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - private static partial int GetErrorCore(ref NativeErrorBuffer output); - - [LibraryImport("ArcPdfNative", EntryPoint = "arc_pdf_open")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial int Open(NativeIoV1* io, NativeStringView password, NativeLimitsV1* limits, ulong* document, uint* pages, NativeCancelToken* cancel); - - [LibraryImport("ArcPdfNative", EntryPoint = "arc_pdf_page_info")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial int PageInfo(ulong document, uint index, NativePdfPageV1* page); - - [LibraryImport("ArcPdfNative", EntryPoint = "arc_pdf_render")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial int Render(ulong document, NativePdfPageV1* page, NativeRegionV1* region, uint fullWidth, uint fullHeight, NativeBuffer* rgba8, NativeCancelToken* cancel); - - [LibraryImport("ArcPdfNative", EntryPoint = "arc_pdf_text")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial int Text(ulong document, uint page, uint start, uint count, NativeBuffer* textGeometry, NativeCancelToken* cancel); - - [LibraryImport("ArcPdfNative", EntryPoint = "arc_pdf_close")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial int Close(ulong document); - - internal static NativeError LastError() => GetLastError(); -} diff --git a/src/Native/ArcForges.Native.Pdf/PdfDocument.cs b/src/Native/ArcForges.Native.Pdf/PdfDocument.cs deleted file mode 100644 index 89d8116b..00000000 --- a/src/Native/ArcForges.Native.Pdf/PdfDocument.cs +++ /dev/null @@ -1,359 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only - -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using ArcForges.Native.Abstractions; - -namespace ArcForges.Native.Pdf; - -/// -/// A document opened through arc_pdf_open. The native library reads the input through callbacks into , so the -/// input must stay valid until this object is disposed. Every failure is a carrying the closed ABI status, -/// or an for a cancellation. No native pointer or handle value is ever exposed. -/// -internal sealed unsafe class PdfDocument : IPdfDocument -{ - private const int MaxTextResponseBytes = 64 * 1024 * 1024; - private const int NativeStructVersion = 1; - - private readonly object _gate = new(); - private readonly GCHandle _inputHandle; - private ulong _handle; - - private PdfDocument(ulong handle, uint pages, GCHandle inputHandle) - { - _handle = handle; - PageCount = pages; - _inputHandle = inputHandle; - } - - /// - public uint PageCount { get; } - - /// - /// Opens a document. An empty password is allowed; a password that is required but missing or wrong is a - /// failure. A library with no PDF backend linked fails with . - /// - public static PdfDocument Open(IPdfInput input, PdfLimits limits, ReadOnlySpan password, CancellationToken cancellation) - { - ArgumentNullException.ThrowIfNull(input); - ArgumentNullException.ThrowIfNull(limits); - if (input.Length <= 0) - { - throw new PdfNativeException(NativeStatus.Corrupt, "The input is empty."); - } - - cancellation.ThrowIfCancellationRequested(); - var inputHandle = GCHandle.Alloc(new InputBridge(input)); - var cancelHandle = GCHandle.Alloc(new CancelBridge(cancellation)); - try - { - NativeIoV1 io = new() - { - StructSize = (uint)sizeof(NativeIoV1), - StructVersion = NativeStructVersion, - Context = GCHandle.ToIntPtr(inputHandle), - Length = (ulong)input.Length, - MaxLength = (ulong)input.Length, - ReadAt = &ReadAtCallback, - }; - NativeLimitsV1 native = ToNative(limits); - NativeCancelToken token = CancelToken(cancelHandle); - var (status, handle, pages) = NativeOpen(&io, password, &native, &token); - if (status != 0) - { - throw Failure(status, cancellation); - } - - if (handle == 0 || pages == 0) - { - if (handle != 0) - { - _ = PdfAbi.Close(handle); - } - - throw new PdfNativeException(NativeStatus.Internal, "The native open reported success without a document."); - } - - return new PdfDocument(handle, pages, inputHandle); - } - catch - { - inputHandle.Free(); - throw; - } - finally - { - cancelHandle.Free(); - } - } - - /// - public PdfPageGeometry GetPage(uint pageIndex) - { - var handle = Live(); - NativePdfPageV1 page = NewPage(0); - var status = PdfAbi.PageInfo(handle, pageIndex, &page); - if (status != 0) - { - throw Failure(status, CancellationToken.None); - } - - if (page.PageIndex != pageIndex || !double.IsFinite(page.WidthPoints) || !double.IsFinite(page.HeightPoints) || page.WidthPoints <= 0 || page.HeightPoints <= 0 - || page.Rotation is not (0 or 90 or 180 or 270)) - { - throw new PdfNativeException(NativeStatus.Corrupt, "The native page geometry is invalid."); - } - - return new PdfPageGeometry(pageIndex, page.Rotation, page.WidthPoints, page.HeightPoints); - } - - /// - public PdfTextChunk ExtractText(uint pageIndex, uint start, uint count, CancellationToken cancellation) - { - var handle = Live(); - var cancelHandle = GCHandle.Alloc(new CancelBridge(cancellation)); - try - { - NativeCancelToken token = CancelToken(cancelHandle); - for (var attempt = 0; attempt < 3; attempt++) - { - NativeBuffer query = default; - var status = PdfAbi.Text(handle, pageIndex, start, count, &query, &token); - if (status != (int)NativeStatus.BufferTooSmall) - { - throw status == 0 - ? new PdfNativeException(NativeStatus.Internal, "The native text query reported success without a size.") - : Failure(status, cancellation); - } - - if (query.Required == 0 || query.Required > MaxTextResponseBytes) - { - throw new PdfNativeException(NativeStatus.ResourceLimit, "The native text response size is out of bounds."); - } - - var bytes = GC.AllocateUninitializedArray((int)query.Required); - fixed (byte* data = bytes) - { - NativeBuffer output = new() { Data = (nint)data, Capacity = (ulong)bytes.Length }; - status = PdfAbi.Text(handle, pageIndex, start, count, &output, &token); - if (status == (int)NativeStatus.BufferTooSmall) - { - continue; // the page text changed between the two calls; ask again - } - - if (status != 0) - { - throw Failure(status, cancellation); - } - - if (output.Required != (ulong)bytes.Length) - { - throw new PdfNativeException(NativeStatus.Internal, "The native text size changed."); - } - } - - return PdfTextJson.Parse(bytes, pageIndex, start); - } - - throw new PdfNativeException(NativeStatus.Busy, "The native text size kept changing."); - } - finally - { - cancelHandle.Free(); - } - } - - /// - public int Render(PdfPageGeometry page, uint x, uint y, uint width, uint height, uint fullWidth, uint fullHeight, uint rowStride, Span destination, CancellationToken cancellation) - { - var handle = Live(); - var cancelHandle = GCHandle.Alloc(new CancelBridge(cancellation)); - try - { - NativeCancelToken token = CancelToken(cancelHandle); - NativePdfPageV1 nativePage = NewPage(page.PageIndex); - nativePage.Rotation = page.Rotation; - nativePage.WidthPoints = page.WidthPoints; - nativePage.HeightPoints = page.HeightPoints; - NativeRegionV1 region = new() - { - StructSize = (uint)sizeof(NativeRegionV1), - StructVersion = NativeStructVersion, - X = x, - Y = y, - Width = width, - Height = height, - RowStride = rowStride, - }; - fixed (byte* data = destination) - { - NativeBuffer buffer = new() { Data = (nint)data, Capacity = (ulong)destination.Length }; - var status = PdfAbi.Render(handle, &nativePage, ®ion, fullWidth, fullHeight, &buffer, &token); - if (status != 0) - { - throw Failure(status, cancellation); - } - - if (buffer.Required > (ulong)destination.Length || buffer.Required > int.MaxValue) - { - throw new PdfNativeException(NativeStatus.Internal, "The native render reported more bytes than it was given."); - } - - return (int)buffer.Required; - } - } - finally - { - cancelHandle.Free(); - } - } - - /// Closes the document and releases the input callbacks. The native close waits for calls that are still running. - public void Dispose() - { - ulong handle; - lock (_gate) - { - handle = _handle; - _handle = 0; - } - - if (handle == 0) - { - return; - } - - _ = PdfAbi.Close(handle); - _inputHandle.Free(); - } - - private static (int Status, ulong Handle, uint Pages) NativeOpen(NativeIoV1* io, ReadOnlySpan password, NativeLimitsV1* limits, NativeCancelToken* token) - { - ulong handle = 0; - uint pages = 0; - int status; - fixed (byte* secret = password) - { - NativeStringView view = new() { Data = (nint)secret, Size = (ulong)password.Length }; - status = PdfAbi.Open(io, view, limits, &handle, &pages, token); - } - - return (status, handle, pages); - } - - private ulong Live() - { - lock (_gate) - { - return _handle != 0 ? _handle : throw new ObjectDisposedException(nameof(PdfDocument)); - } - } - - private static NativePdfPageV1 NewPage(uint index) => new() - { - StructSize = (uint)sizeof(NativePdfPageV1), - StructVersion = NativeStructVersion, - PageIndex = index, - }; - - private static NativeLimitsV1 ToNative(PdfLimits limits) => new() - { - StructSize = (uint)sizeof(NativeLimitsV1), - StructVersion = NativeStructVersion, - MaxInputBytes = limits.MaxInputBytes, - MaxMemoryBytes = limits.MaxMemoryBytes, - MaxOutputBytes = limits.MaxOutputBytes, - MaxWidth = limits.MaxWidth, - MaxHeight = limits.MaxHeight, - MaxItems = limits.MaxItems, - TimeoutMs = limits.TimeoutMs, - }; - - private static NativeCancelToken CancelToken(GCHandle cancelHandle) => new() - { - StructSize = (uint)sizeof(NativeCancelToken), - StructVersion = NativeStructVersion, - IsCancelled = &IsCancelledCallback, - UserData = GCHandle.ToIntPtr(cancelHandle), - }; - - private static Exception Failure(int status, CancellationToken cancellation) - { - if (status == (int)NativeStatus.Cancelled) - { - return new OperationCanceledException(cancellation); - } - - var message = string.Empty; - try - { - message = PdfAbi.LastError().Message; - } - catch (InvalidOperationException) - { - // The diagnostic is best effort; the status is the contract. - } - - var known = Enum.IsDefined(typeof(NativeStatus), status) ? (NativeStatus)status : NativeStatus.Internal; - return new PdfNativeException(known, message); - } - - [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] - [SuppressMessage("Design", "CA1031", Justification = "An exception must never cross the native frame; it becomes the closed I/O status.")] - private static int ReadAtCallback(nint context, ulong offset, nint destination, ulong requested, ulong* done) - { - try - { - if (done is null) - { - return (int)NativeStatus.InvalidArgument; - } - - *done = 0; - var bridge = (InputBridge?)GCHandle.FromIntPtr(context).Target; - if (bridge is null || requested > int.MaxValue || offset > (ulong)long.MaxValue) - { - return (int)NativeStatus.InvalidArgument; - } - - var count = bridge.Input.ReadAt((long)offset, new Span((void*)destination, (int)requested)); - if (count < 0 || (ulong)count > requested) - { - return (int)NativeStatus.Io; - } - - *done = (ulong)count; - return 0; - } - catch (Exception) - { - return (int)NativeStatus.Io; - } - } - - [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] - [SuppressMessage("Design", "CA1031", Justification = "An exception must never cross the native frame; an unreadable token counts as cancelled.")] - private static byte IsCancelledCallback(nint userData) - { - try - { - return ((CancelBridge?)GCHandle.FromIntPtr(userData).Target)?.Token.IsCancellationRequested == false ? (byte)0 : (byte)1; - } - catch (Exception) - { - return 1; - } - } - - private sealed class InputBridge(IPdfInput input) - { - internal IPdfInput Input { get; } = input; - } - - private sealed class CancelBridge(CancellationToken token) - { - internal CancellationToken Token { get; } = token; - } -} diff --git a/src/Native/ArcForges.Native.Pdf/PdfTextJson.cs b/src/Native/ArcForges.Native.Pdf/PdfTextJson.cs deleted file mode 100644 index fa5cd534..00000000 --- a/src/Native/ArcForges.Native.Pdf/PdfTextJson.cs +++ /dev/null @@ -1,165 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only - -using System.Text.Json; - -namespace ArcForges.Native.Pdf; - -/// -/// Reads the closed text encoding of arc_pdf_text: {version,page,start,next?,text,boxes:[{start,length,x,y,width,height}]}. Reading is -/// strict and independent of the native engine's own checks: an unknown or repeated member, a wrong type, a box outside the chunk window or a -/// non-finite number refuses the whole chunk. -/// -internal static class PdfTextJson -{ - internal static PdfTextChunk Parse(ReadOnlySpan utf8, uint expectedPage, uint expectedStart) - { - var reader = new Utf8JsonReader(utf8, new JsonReaderOptions { CommentHandling = JsonCommentHandling.Disallow, AllowTrailingCommas = false, MaxDepth = 8 }); - try - { - return Read(ref reader, expectedPage, expectedStart); - } - catch (JsonException exception) - { - throw new PdfNativeException("The native text encoding is malformed.", exception); - } - catch (InvalidOperationException exception) - { - throw new PdfNativeException("The native text encoding is malformed.", exception); - } - catch (FormatException exception) - { - throw new PdfNativeException("The native text encoding is malformed.", exception); - } - } - - private static PdfTextChunk Read(ref Utf8JsonReader reader, uint expectedPage, uint expectedStart) - { - Expect(ref reader, JsonTokenType.StartObject); - uint? version = null; - uint? page = null; - uint? start = null; - uint? next = null; - string? text = null; - List? boxes = null; - while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName) - { - var name = reader.GetString(); - _ = reader.Read(); - switch (name) - { - case "version" when version is null: - version = reader.GetUInt32(); - break; - case "page" when page is null: - page = reader.GetUInt32(); - break; - case "start" when start is null: - start = reader.GetUInt32(); - break; - case "next" when next is null: - next = reader.GetUInt32(); - break; - case "text" when text is null: - text = reader.GetString() ?? throw new PdfNativeException("The native text is null."); - break; - case "boxes" when boxes is null: - boxes = ReadBoxes(ref reader); - break; - default: - throw new PdfNativeException("The native text encoding has an unknown or repeated member."); - } - } - - if (reader.TokenType != JsonTokenType.EndObject || reader.Read() || version != 1 || page != expectedPage || start != expectedStart || text is null || boxes is null) - { - throw new PdfNativeException("The native text encoding is incomplete or answers another request."); - } - - var end = (ulong)start.Value + (ulong)text.Length; - if (next is not null && (next.Value != end || next.Value <= start.Value)) - { - throw new PdfNativeException("The native text chunk makes no progress."); - } - - foreach (var box in boxes) - { - if (box.Start < start.Value || (ulong)box.Start + box.Length > end || box.Length == 0) - { - throw new PdfNativeException("A native text box lies outside its chunk."); - } - } - - return new PdfTextChunk(page.Value, start.Value, next, text, boxes); - } - - private static List ReadBoxes(ref Utf8JsonReader reader) - { - if (reader.TokenType != JsonTokenType.StartArray) - { - throw new PdfNativeException("The native box list has the wrong shape."); - } - - var boxes = new List(); - while (reader.Read() && reader.TokenType == JsonTokenType.StartObject) - { - uint? start = null; - uint? length = null; - double? x = null; - double? y = null; - double? width = null; - double? height = null; - while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName) - { - var name = reader.GetString(); - _ = reader.Read(); - switch (name) - { - case "start" when start is null: - start = reader.GetUInt32(); - break; - case "length" when length is null: - length = reader.GetUInt32(); - break; - case "x" when x is null: - x = Finite(reader.GetDouble()); - break; - case "y" when y is null: - y = Finite(reader.GetDouble()); - break; - case "width" when width is null: - width = Finite(reader.GetDouble()); - break; - case "height" when height is null: - height = Finite(reader.GetDouble()); - break; - default: - throw new PdfNativeException("A native text box has an unknown or repeated member."); - } - } - - if (reader.TokenType != JsonTokenType.EndObject || start is null || length is null || x is null || y is null || width is null || height is null || width < 0 || height < 0) - { - throw new PdfNativeException("A native text box is incomplete."); - } - - boxes.Add(new PdfTextBox(start.Value, length.Value, x.Value, y.Value, width.Value, height.Value)); - } - - if (reader.TokenType != JsonTokenType.EndArray) - { - throw new PdfNativeException("The native box list is malformed."); - } - - return boxes; - } - - private static double Finite(double value) => double.IsFinite(value) ? value : throw new PdfNativeException("A native number is not finite."); - - private static void Expect(ref Utf8JsonReader reader, JsonTokenType type) - { - if (!reader.Read() || reader.TokenType != type) - { - throw new PdfNativeException("The native text encoding has the wrong shape."); - } - } -} diff --git a/src/Native/ArcForges.Native.Pdf/PdfTypes.cs b/src/Native/ArcForges.Native.Pdf/PdfTypes.cs deleted file mode 100644 index f34985b5..00000000 --- a/src/Native/ArcForges.Native.Pdf/PdfTypes.cs +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only - -using System.Diagnostics.CodeAnalysis; -using ArcForges.Native.Abstractions; - -namespace ArcForges.Native.Pdf; - -/// The immutable PDF bytes. The native library reads them through this and never past . -internal interface IPdfInput -{ - /// The byte length of the input. - long Length { get; } - - /// Reads up to .Length bytes at and returns how many were read; 0 only at the end. - int ReadAt(long offset, Span destination); -} - -/// The budget of one document. Zero is never valid; the native profile maxima apply on top. -internal sealed record PdfLimits( - ulong MaxInputBytes, - ulong MaxMemoryBytes, - ulong MaxOutputBytes, - uint MaxWidth, - uint MaxHeight, - uint MaxItems, - uint TimeoutMs); - -/// The geometry of one page in PDF points; rotation is 0, 90, 180 or 270. -internal readonly record struct PdfPageGeometry(uint PageIndex, uint Rotation, double WidthPoints, double HeightPoints); - -/// A text rectangle. Start and length are UTF-16 offsets into the full page text. -internal readonly record struct PdfTextBox(uint Start, uint Length, double X, double Y, double Width, double Height); - -/// One bounded chunk of page text. is the start of the following chunk, or null at the end of the page. -internal sealed record PdfTextChunk(uint Page, uint Start, uint? Next, string Text, IReadOnlyList Boxes); - -/// A PDF document opened with scripts and actions disabled. Implemented by and by test doubles. -internal interface IPdfDocument : IDisposable -{ - /// The number of pages, at least one. - uint PageCount { get; } - - /// The geometry of a page. - PdfPageGeometry GetPage(uint pageIndex); - - /// Up to UTF-16 units of page text from . - PdfTextChunk ExtractText(uint pageIndex, uint start, uint count, CancellationToken cancellation); - - /// Renders the tile into as RGBA8 at the given row stride and returns the number of bytes written. - int Render(PdfPageGeometry page, uint x, uint y, uint width, uint height, uint fullWidth, uint fullHeight, uint rowStride, Span destination, CancellationToken cancellation); -} - -/// A native PDF call failed. The status is the closed ABI status; the message is diagnostic only and is never sent to a peer. -[SuppressMessage("Design", "CA1064", Justification = "The binding is internal to the helper (no public API surface); the exception stays beside it.")] -internal sealed class PdfNativeException : Exception -{ - public PdfNativeException() - : this(NativeStatus.Internal, string.Empty) - { - } - - public PdfNativeException(string message) - : this(NativeStatus.Internal, message) - { - } - - public PdfNativeException(string message, Exception innerException) - : base(message, innerException) - { - Status = NativeStatus.Internal; - } - - public PdfNativeException(NativeStatus status, string message) - : base(message) - { - Status = status; - } - - /// The ABI status. - public NativeStatus Status { get; } -} diff --git a/src/Native/ArcForges.Native.Pdf/README.md b/src/Native/ArcForges.Native.Pdf/README.md deleted file mode 100644 index 27afd7db..00000000 --- a/src/Native/ArcForges.Native.Pdf/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# ArcForges.Native.Pdf - -Source-generated binding of the `arc_pdf_*` ABI of `ArcPdfNative` (`native/arcpdf-abi`): open a PDF through caller-supplied -read callbacks, read page geometry, extract bounded text with geometry, and render RGBA8 tiles. Every call is bounded by the -document limits and a cancellation token, and every native failure is a `PdfNativeException` with the closed ABI status. -No native pointer or handle value leaves this assembly. - -**Not packable and not a capability yet.** `native/arcpdf-abi` links no PDF parser in this delivery: a library built from it -reports `backend=none` in its build information and refuses every open with `NativeStatus.Unsupported`. The real PDFium -backend, the `Runtime.` packages and the package entry are separate, later work. Do not publish this project as a package -before that. - -Text encoding: `start` and `count` of `arc_pdf_text` are UTF-16 code-unit offsets into the full page text (at most 65536 per -call); a chunk never splits a surrogate pair; box offsets are absolute offsets into the page text, and a chunk carries only the -boxes wholly inside it. - -The binding is consumed only by the ContentSandbox helper (`src/DesktopHelpers/ArcForges.ContentSandbox`): PDFium belongs -inside the restricted helper process, never in a product process. diff --git a/src/Native/ArcForges.Native.Pdf/packages.lock.json b/src/Native/ArcForges.Native.Pdf/packages.lock.json deleted file mode 100644 index a6ff152e..00000000 --- a/src/Native/ArcForges.Native.Pdf/packages.lock.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "version": 2, - "dependencies": { - "net10.0": { - "Microsoft.CodeAnalysis.NetAnalyzers": { - "type": "Direct", - "requested": "[10.0.401, )", - "resolved": "10.0.401", - "contentHash": "qGPLk4hknvVp6j9Lowa0qykw1cK7CyEyyy2aVs4dkOGroOMHJLRsMpUT53IKvYT6q78jxv96FZhzy3BrKCPgmA==" - }, - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[10.0.11, )", - "resolved": "10.0.11", - "contentHash": "IBf7lbovvjGWVWXZX5cJ/cO0WXbId0Zq4BuSeT94mGZuOAP66oMeH9PTBZ9Jpp3Jb6jtK0qm/NyUbPRo1gC/wQ==" - }, - "arcforges.native.abstractions": { - "type": "Project" - } - } - } -} \ No newline at end of file diff --git a/tests/ArchitectureTests/RepositoryPolicyTests.cs b/tests/ArchitectureTests/RepositoryPolicyTests.cs index 8800fad9..ac60c99f 100644 --- a/tests/ArchitectureTests/RepositoryPolicyTests.cs +++ b/tests/ArchitectureTests/RepositoryPolicyTests.cs @@ -54,7 +54,6 @@ public void ProductionNativeBindingsHaveOneCapabilityOwner() // The closed map: every production binding is one library:entry-point export with exactly one owner directory and exactly its declared // marshalling flags (UTF-16 strings, last-error capture). A binding that is not listed, is listed twice or is declared elsewhere fails. string image = Path.Combine(Root, "src", "Native", "ArcForges.Native.Image"); - string pdf = Path.Combine(Root, "src", "Native", "ArcForges.Native.Pdf"); string secrets = Path.Combine(Root, "src", "BuildingBlocks", "ArcForges.Security.Secrets"); string broker = Path.Combine(Root, "src", "DesktopHelpers", "ArcForges.ContentSandbox.Broker", "Native"); string helper = Path.Combine(Root, "src", "DesktopHelpers", "ArcForges.ContentSandbox", "Native"); @@ -67,14 +66,6 @@ public void ProductionNativeBindingsHaveOneCapabilityOwner() ["ArcImageNative:arc_image_get_abi_version"] = (image, false, false), ["ArcImageNative:arc_image_get_build_info"] = (image, false, false), ["ArcImageNative:arc_image_get_last_error"] = (image, false, false), - ["ArcPdfNative:arc_pdf_get_abi_version"] = (pdf, false, false), - ["ArcPdfNative:arc_pdf_get_build_info"] = (pdf, false, false), - ["ArcPdfNative:arc_pdf_get_last_error"] = (pdf, false, false), - ["ArcPdfNative:arc_pdf_open"] = (pdf, false, false), - ["ArcPdfNative:arc_pdf_page_info"] = (pdf, false, false), - ["ArcPdfNative:arc_pdf_render"] = (pdf, false, false), - ["ArcPdfNative:arc_pdf_text"] = (pdf, false, false), - ["ArcPdfNative:arc_pdf_close"] = (pdf, false, false), ["Advapi32.dll:EqualSid"] = (broker, false, false), ["Advapi32.dll:FreeSid"] = (broker, false, false), ["Advapi32.dll:GetTokenInformation"] = (broker, false, true), diff --git a/tests/NativeAbiTests/CommonAbiLayoutTests.cs b/tests/NativeAbiTests/CommonAbiLayoutTests.cs index 73ccb0f3..0163f625 100644 --- a/tests/NativeAbiTests/CommonAbiLayoutTests.cs +++ b/tests/NativeAbiTests/CommonAbiLayoutTests.cs @@ -9,7 +9,7 @@ public sealed class CommonAbiLayoutTests { [Xunit.Fact] [Xunit.Trait("Category", "NativeAbiLayout")] - public void ManagedLayoutsMatchTheSeventeenNormativeSizesAndEveryFieldOffset() + public void ManagedLayoutsMatchTheSixteenNormativeSizesAndEveryFieldOffset() { Xunit.Assert.Equal(8, IntPtr.Size); @@ -50,13 +50,10 @@ public void ManagedLayoutsMatchTheSeventeenNormativeSizesAndEveryFieldOffset() ("Index", 32), ("Reserved", 36)]), ("NativeImageOptionsV1", 72, [("StructSize", 0), ("StructVersion", 4), ("Subimage", 8), ("Mip", 12), - ("Format", 16), ("Reserved", 20), ("Limits", 24)]), - ("NativePdfPageV1", 32, - [("StructSize", 0), ("StructVersion", 4), ("PageIndex", 8), ("Rotation", 12), - ("WidthPoints", 16), ("HeightPoints", 24)]) + ("Format", 16), ("Reserved", 20), ("Limits", 24)]) ]; - Xunit.Assert.Equal(17, layouts.Length + 1); // The enum-sized arc_status_t is counted separately. + Xunit.Assert.Equal(16, layouts.Length + 1); // The enum-sized arc_status_t is counted separately. Type statusUnderlyingType = Enum.GetUnderlyingType(typeof(NativeStatus)); Xunit.Assert.Equal(typeof(int), statusUnderlyingType); Xunit.Assert.Equal(4, Marshal.SizeOf(statusUnderlyingType)); diff --git a/win.slnx b/win.slnx index 5ca2817c..0287be17 100644 --- a/win.slnx +++ b/win.slnx @@ -36,6 +36,5 @@ - \ No newline at end of file