@reekon-tools/react-native-pdf-canvas 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +57 -0
  3. package/PdfCanvas.podspec +68 -39
  4. package/README.md +213 -1316
  5. package/android/build.gradle +67 -54
  6. package/android/consumer-rules.pro +7 -0
  7. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
  8. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
  9. package/android/src/main/cpp/CMakeLists.txt +54 -0
  10. package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
  11. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
  12. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
  13. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
  14. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
  15. package/android/tools/compile-gate.sh +81 -101
  16. package/dist/controller.d.ts +14 -0
  17. package/dist/controller.js +4 -3
  18. package/dist/rasterizer/index.native.d.ts +8 -12
  19. package/dist/rasterizer/index.native.js +15 -21
  20. package/dist/rasterizer/native-bridge.d.ts +51 -75
  21. package/dist/rasterizer/native-bridge.js +49 -33
  22. package/dist/rasterizer/native.d.ts +45 -0
  23. package/dist/rasterizer/native.js +123 -0
  24. package/dist/react/usePdfDocument.d.ts +5 -5
  25. package/dist/react/usePdfLayer.d.ts +10 -1
  26. package/dist/react/usePdfLayer.js +10 -2
  27. package/dist/testing/scenes.d.ts +3 -3
  28. package/dist/testing/scenes.js +4 -4
  29. package/dist/types.d.ts +36 -42
  30. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
  31. package/native/.clangd +5 -0
  32. package/native/CMakeLists.txt +89 -0
  33. package/native/core/include/pdfcanvas/document.h +90 -0
  34. package/native/core/include/pdfcanvas/error.h +47 -0
  35. package/native/core/include/pdfcanvas/jsi.h +18 -0
  36. package/native/core/include/pdfcanvas/jsi_config.h +30 -0
  37. package/native/core/include/pdfcanvas/library.h +28 -0
  38. package/native/core/include/pdfcanvas/pixels.h +92 -0
  39. package/native/core/include/pdfcanvas/service.h +87 -0
  40. package/native/core/include/pdfcanvas/slots.h +113 -0
  41. package/native/core/include/pdfcanvas/types.h +110 -0
  42. package/native/core/pdfcanvas-core.cmake +22 -0
  43. package/native/core/src/document.cpp +516 -0
  44. package/native/core/src/error.cpp +29 -0
  45. package/native/core/src/jsi.cpp +66 -0
  46. package/native/core/src/library.cpp +52 -0
  47. package/native/core/src/pixels.cpp +45 -0
  48. package/native/core/src/service.cpp +156 -0
  49. package/native/core/src/slots.cpp +139 -0
  50. package/native/tests/fixtures.cpp +451 -0
  51. package/native/tests/fixtures.h +96 -0
  52. package/native/tests/harness.h +122 -0
  53. package/native/tests/main.cpp +64 -0
  54. package/native/tests/pixels_util.h +126 -0
  55. package/native/tests/test_document.cpp +485 -0
  56. package/native/tests/test_service.cpp +130 -0
  57. package/native/tests/test_slots.cpp +250 -0
  58. package/package.json +27 -7
  59. package/react-native.config.js +5 -2
  60. package/scripts/fetch-pdfium.mjs +487 -0
  61. package/scripts/pdfium-manifest.json +46 -0
  62. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
  63. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
  64. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
  65. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
  66. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
  67. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
  68. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
  69. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
  70. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
  71. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
  72. package/android/src/jsi/cpp/CMakeLists.txt +0 -33
  73. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
  74. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
  75. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
  76. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
  77. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
  78. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
  79. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
  80. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
  81. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
  82. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
  83. package/dist/rasterizer/android.d.ts +0 -121
  84. package/dist/rasterizer/android.js +0 -126
  85. package/dist/rasterizer/ios.d.ts +0 -84
  86. package/dist/rasterizer/ios.js +0 -155
  87. package/ios/Package.swift +0 -58
  88. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
  89. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
  90. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
  91. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
  92. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
  93. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
  94. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
  95. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
  96. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
  97. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
  98. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
  99. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
  100. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
  101. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
  102. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
  103. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
  104. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
  105. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
  106. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
  107. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
  108. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
  109. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
  110. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
  111. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
  112. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
  113. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
  114. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
  115. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
  116. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
  117. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
  118. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
  119. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
  120. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
  121. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
@@ -1,98 +0,0 @@
1
- package tools.reekon.pdfcanvas;
2
-
3
- import static org.junit.Assert.assertEquals;
4
- import static org.junit.Assert.assertTrue;
5
- import static org.junit.Assert.fail;
6
-
7
- import android.content.Context;
8
- import android.os.Build;
9
- import android.util.Log;
10
-
11
- import androidx.test.ext.junit.runners.AndroidJUnit4;
12
- import androidx.test.platform.app.InstrumentationRegistry;
13
-
14
- import org.junit.Test;
15
- import org.junit.runner.RunWith;
16
-
17
- import java.io.File;
18
-
19
- /**
20
- * The end-to-end proof of the poison guard: BREAK the platform for real, then
21
- * show that the backend refuses instead of segfaulting.
22
- *
23
- * EXCLUDED from `connectedDebugAndroidTest` by the `notAnnotation` filter in
24
- * `android/build.gradle`, because on API 24 it leaves PDFium unusable for the
25
- * rest of the process and every test scheduled after it would die. Run it alone:
26
- *
27
- * <pre>
28
- * ./gradlew installDebugAndroidTest
29
- * adb -s &lt;device&gt; shell am instrument -w \
30
- * -e class tools.reekon.pdfcanvas.PdfCanvasPoisonGuardTest \
31
- * -e annotation tools.reekon.pdfcanvas.PoisonsTheProcess \
32
- * tools.reekon.pdfcanvas.test/androidx.test.runner.AndroidJUnitRunner
33
- * </pre>
34
- *
35
- * The fixture is a file with a real {@code %PDF-} header and unparseable
36
- * contents — a truncated download, in other words. Preflight cannot reject it,
37
- * which is the point: it is precisely the residue preflight leaves behind, and
38
- * therefore the only case the latch has to handle.
39
- */
40
- @RunWith(AndroidJUnit4.class)
41
- public class PdfCanvasPoisonGuardTest {
42
-
43
- private static final String TAG = "PDFCANVAS";
44
-
45
- private static Context ctx() {
46
- return InstrumentationRegistry.getInstrumentation().getTargetContext();
47
- }
48
-
49
- @Test
50
- @PoisonsTheProcess
51
- public void aFileThatPreflightCannotJudgeIsHandledWithoutCrashing() throws Exception {
52
- File garbage = TestPdfs.headerOnlyGarbage(ctx(), "poison-header-only.pdf");
53
- File good = TestPdfs.pattern(ctx(), "poison-good.pdf", 100, 100);
54
-
55
- PdfPlatformQuirks.setPdfiumPoisonedForTesting(false);
56
-
57
- // Opening it must fail as CORRUPT — preflight let it through, PdfRenderer
58
- // rejected it.
59
- try {
60
- PdfCanvasRasterizer.openFile(garbage, null, 1).close();
61
- fail("header-only garbage should not have opened");
62
- } catch (PdfRasterException e) {
63
- assertEquals(PdfErrorCode.CORRUPT, e.code());
64
- }
65
-
66
- boolean poisoned = PdfPlatformQuirks.isPdfiumPoisoned();
67
- Log.i(TAG, "poison-guard: API " + Build.VERSION.SDK_INT + " latched=" + poisoned);
68
- assertEquals(
69
- "the latch should have been set exactly on the platforms treated as affected",
70
- PdfPlatformQuirks.failedNativeOpenIsPoisonous(),
71
- poisoned);
72
-
73
- // Force the collection that actually corrupts PDFium's reference count. On
74
- // an unguarded build, the next open is the SIGSEGV.
75
- for (int i = 0; i < 3; i++) {
76
- System.gc();
77
- System.runFinalization();
78
- Thread.sleep(150);
79
- }
80
-
81
- if (poisoned) {
82
- try {
83
- PdfCanvasRasterizer.openFile(good, null, 1).close();
84
- fail("a poisoned process must refuse rather than segfault");
85
- } catch (PdfRasterException e) {
86
- assertEquals(PdfErrorCode.BACKEND_FAILURE, e.code());
87
- assertTrue(e.getMessage(), e.getMessage().contains("PDFium"));
88
- }
89
- Log.i(TAG, "poison-guard: refused after real poisoning, process alive");
90
- } else {
91
- // Unaffected platform: opening must still work after the collection.
92
- PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(good, null, 1);
93
- assertEquals(1, r.pageCount());
94
- r.close();
95
- Log.i(TAG, "poison-guard: platform unaffected, opened normally after GC");
96
- }
97
- }
98
- }
@@ -1,324 +0,0 @@
1
- package tools.reekon.pdfcanvas;
2
-
3
- import static org.junit.Assert.assertEquals;
4
- import static org.junit.Assert.assertFalse;
5
- import static org.junit.Assert.assertNotEquals;
6
- import static org.junit.Assert.assertNull;
7
- import static org.junit.Assert.assertTrue;
8
- import static org.junit.Assert.fail;
9
-
10
- import android.content.Context;
11
- import android.os.Build;
12
- import android.util.Log;
13
-
14
- import androidx.test.ext.junit.runners.AndroidJUnit4;
15
- import androidx.test.platform.app.InstrumentationRegistry;
16
-
17
- import org.junit.Test;
18
- import org.junit.runner.RunWith;
19
-
20
- import java.io.File;
21
-
22
- /**
23
- * The two measured platform quirks, and the guards built for them.
24
- *
25
- * These tests are the reason the guards are not superstition. Each one either
26
- * asserts a fact measured on a device in this suite's own run, or asserts that
27
- * the guard reports that fact truthfully.
28
- */
29
- @RunWith(AndroidJUnit4.class)
30
- public class PdfCanvasQuirksTest {
31
-
32
- private static final String TAG = "PDFCANVAS";
33
-
34
- private static Context ctx() {
35
- return InstrumentationRegistry.getInstrumentation().getTargetContext();
36
- }
37
-
38
- /**
39
- * THE test that justifies {@link PdfPreflight} existing.
40
- *
41
- * CATCHES: a bad file reaching {@code new PdfRenderer(...)}. On API 24 that
42
- * constructor throwing is a delayed SIGSEGV for the whole process, so
43
- * "produces the right error code" is not enough — the file must be turned away
44
- * before the platform class sees it, and the observable proof is that the
45
- * poison latch is still clear afterwards.
46
- *
47
- * A backend that dropped preflight would still return the same three codes
48
- * here, and would still pass every assertion in PdfCanvasFailureTest. Only the
49
- * latch assertion notices.
50
- */
51
- @Test
52
- public void preflightTurnsBadFilesAwayBeforePdfRendererSeesThem() throws Exception {
53
- PdfPlatformQuirks.setPdfiumPoisonedForTesting(false);
54
-
55
- assertEquals(PdfErrorCode.CORRUPT, openCode(TestPdfs.notAPdf(ctx(), "q-notes.txt")));
56
- assertFalse("a non-PDF reached PdfRenderer", PdfPlatformQuirks.isPdfiumPoisoned());
57
-
58
- assertEquals(PdfErrorCode.CORRUPT, openCode(TestPdfs.empty(ctx(), "q-empty.pdf")));
59
- assertFalse("an empty file reached PdfRenderer", PdfPlatformQuirks.isPdfiumPoisoned());
60
-
61
- assertEquals(
62
- PdfErrorCode.PASSWORD_REQUIRED, openCode(TestPdfs.encrypted(ctx(), "q-locked.pdf")));
63
- assertFalse("an encrypted file reached PdfRenderer", PdfPlatformQuirks.isPdfiumPoisoned());
64
-
65
- Log.i(TAG, "preflight: three bad files rejected, latch still clear on API "
66
- + Build.VERSION.SDK_INT);
67
- }
68
-
69
- /**
70
- * CATCHES: a preflight that never looks at the trailer of a MID-SIZED file.
71
- *
72
- * Preflight reads a 32 KB head and a 256 KB tail. Every other fixture in this
73
- * suite is a few hundred bytes, so the head covers them end to end and the
74
- * tail branch is never exercised at all — which is how
75
- * {@code tailStart < head.length} survived: for any length between the two
76
- * windows {@code tailStart} is 0, the tail is dropped as "already covered",
77
- * and everything past 32 KB — INCLUDING THE TRAILER AND ITS /Encrypt
78
- * REFERENCE — is never read. A 60 KB encrypted drawing is an ordinary
79
- * document, and on API 24 it then reaches {@code new PdfRenderer(...)} and
80
- * poisons PDFium for the session.
81
- *
82
- * The discriminator is the CAUSE, not the code. Both preflight and the
83
- * {@code SecurityException} path in {@link PdfRendererPool} report
84
- * {@code password-required} with the same user-facing sentence, deliberately
85
- * — so the code alone cannot say which one answered. Preflight throws from the
86
- * bytes and has no cause; the platform path always wraps the
87
- * {@code SecurityException} it caught. A null cause therefore means the file
88
- * was turned away BEFORE PdfRenderer saw it, which is the whole claim, and it
89
- * is observable on every API level rather than only on the one where the latch
90
- * arms.
91
- */
92
- @Test
93
- public void preflightFindsTheEncryptTrailerBeyondTheHeadWindow() throws Exception {
94
- PdfPlatformQuirks.setPdfiumPoisonedForTesting(false);
95
-
96
- File pdf = TestPdfs.encrypted(ctx(), "q-locked-60k.pdf", 60 * 1024);
97
- long length = pdf.length();
98
- Log.i(TAG, "preflight tail scan: fixture is " + length + " bytes");
99
- assertTrue(
100
- "the fixture must land between the 32 KB head and the 256 KB tail window, "
101
- + "which is the band the bug covered: " + length,
102
- length > 32 * 1024 && length < 256 * 1024);
103
-
104
- try {
105
- PdfCanvasRasterizer.openFile(pdf, null, 1).close();
106
- fail("a " + length + "-byte encrypted document should have been refused");
107
- } catch (PdfRasterException e) {
108
- assertEquals(PdfErrorCode.PASSWORD_REQUIRED, e.code());
109
- assertNull(
110
- "preflight must have answered from the bytes; a cause means the file "
111
- + "reached PdfRenderer and the SecurityException path answered instead: "
112
- + e.getCause(),
113
- e.getCause());
114
- }
115
- assertFalse(
116
- "a " + length + "-byte encrypted file reached PdfRenderer",
117
- PdfPlatformQuirks.isPdfiumPoisoned());
118
- }
119
-
120
- /**
121
- * CATCHES: a preflight so eager it refuses good documents.
122
- *
123
- * The encryption test looks for {@code /Encrypt} followed by an indirect
124
- * reference precisely so that the string appearing in page CONTENT does not
125
- * trip it. This fixture puts that string in an uncompressed content stream,
126
- * where a naive substring search finds it.
127
- */
128
- @Test
129
- public void preflightDoesNotMistakeContentForEncryption() throws Exception {
130
- File pdf = TestPdfs.withLiteralEncryptInContent(ctx(), "q-decoy.pdf");
131
- PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
132
- try {
133
- assertEquals(1, r.pageCount());
134
- RasterPixels px = r.render(
135
- new RasterRequest(0, 0f, 0f, 200f, 200f, 1f, false, RasterRequest.BACKGROUND_WHITE),
136
- null);
137
- assertTrue(
138
- "the decoy document should render normally",
139
- PdfCanvasRasterizerTest.isRed(PdfCanvasRasterizerTest.rgba(px, 100, 100)));
140
- } finally {
141
- r.close();
142
- }
143
- }
144
-
145
- /**
146
- * CATCHES: a latch that is set but never consulted, and one that cannot be
147
- * distinguished from an ordinary backend failure.
148
- *
149
- * The real poisoning is not triggered here — doing so would take the rest of
150
- * the run down with it, which is the whole point of the bug. See
151
- * {@code PdfCanvasPoisonGuardTest}, which does trigger it and is excluded from
152
- * the default run for exactly that reason.
153
- */
154
- @Test
155
- public void poisonLatchRefusesInsteadOfOpening() throws Exception {
156
- File good = TestPdfs.pattern(ctx(), "q-good.pdf", 100, 100);
157
- try {
158
- PdfPlatformQuirks.setPdfiumPoisonedForTesting(true);
159
- try {
160
- PdfCanvasRasterizer.openFile(good, null, 1).close();
161
- fail("a poisoned process must refuse to open a document");
162
- } catch (PdfRasterException e) {
163
- assertEquals(PdfErrorCode.BACKEND_FAILURE, e.code());
164
- assertTrue(
165
- "the message must name the cause, not just fail: " + e.getMessage(),
166
- e.getMessage().contains("PDFium") && e.getMessage().contains("Restarting"));
167
- }
168
- } finally {
169
- PdfPlatformQuirks.setPdfiumPoisonedForTesting(false);
170
- }
171
- // And the guard lifts cleanly, so a stuck latch is caught too.
172
- PdfCanvasRasterizer.openFile(good, null, 1).close();
173
- }
174
-
175
- /**
176
- * The MEASUREMENTS, written down as literals, and the guard checked against
177
- * them.
178
- *
179
- * CATCHES: the poison bound drifting away from what was measured.
180
- *
181
- * The previous version of this test could not: it compared
182
- * {@code failedNativeOpenIsPoisonous()} — whose entire body is
183
- * {@code SDK_INT < LOWEST_SDK_MEASURED_SAFE_AFTER_FAILED_OPEN} — against
184
- * {@code SDK_INT < LOWEST_SDK_MEASURED_SAFE_AFTER_FAILED_OPEN}. Both sides
185
- * move with the constant, so editing it from 31 to 25 (which would silently
186
- * declare five never-tested API levels safe from a process-killing bug) left
187
- * every assertion green. A test whose two sides are the same expression is
188
- * asserting that {@code ==} works.
189
- *
190
- * So the expected values are LITERALS here, taken from the measurement in
191
- * {@link PdfPreflight}'s class comment: API 24 died after one failed
192
- * construction plus a collection, API 31 and 33 survived twelve. The bound is
193
- * the lowest level measured GOOD — 31 — deliberately rather than one above the
194
- * level measured bad, because 25 to 30 were never tested and treating untested
195
- * territory as safe is how a SIGSEGV ships. Changing either the constant or
196
- * the rule now requires changing this test, which is the point: the edit
197
- * becomes visible in review instead of silent.
198
- */
199
- @Test
200
- public void poisonBoundMatchesWhatWasMeasured() {
201
- assertEquals(
202
- "the bound must stay at the lowest API level MEASURED to survive a failed "
203
- + "PdfRenderer construction. 24 was measured to die; 31 and 33 survived. "
204
- + "25-30 were never tested and are deliberately treated as affected.",
205
- 31,
206
- PdfPlatformQuirks.LOWEST_SDK_MEASURED_SAFE_AFTER_FAILED_OPEN);
207
-
208
- boolean poisonous = PdfPlatformQuirks.failedNativeOpenIsPoisonous();
209
- Log.i(TAG, "quirk: API " + Build.VERSION.SDK_INT + " failedNativeOpenIsPoisonous="
210
- + poisonous);
211
-
212
- Boolean measured = measuredPoisonous(Build.VERSION.SDK_INT);
213
- if (measured == null) {
214
- // Not one of the levels this suite has ever run against, so there is no
215
- // measurement to check the guard against. Say so rather than inventing a
216
- // rule and asserting the code against itself again.
217
- Log.i(TAG, "quirk: API " + Build.VERSION.SDK_INT + " was never measured; the "
218
- + "verdict is derived from the bound and is not pinned here");
219
- return;
220
- }
221
- assertEquals(
222
- "API " + Build.VERSION.SDK_INT + " was MEASURED as "
223
- + (measured ? "poisonous" : "safe") + " after a failed open",
224
- measured.booleanValue(),
225
- poisonous);
226
- }
227
-
228
- /**
229
- * @return what a failed {@code PdfRenderer} construction was OBSERVED to do on
230
- * this API level, or null for a level that was never put on a device. See
231
- * the transcript in {@link PdfPreflight}'s class comment.
232
- */
233
- private static Boolean measuredPoisonous(int sdkInt) {
234
- switch (sdkInt) {
235
- case 24:
236
- // SIGSEGV in CPDF_Document after one failed open plus System.gc().
237
- return Boolean.TRUE;
238
- case 31:
239
- case 33:
240
- // SURVIVED 12 failed opens plus forced collections.
241
- return Boolean.FALSE;
242
- default:
243
- return null;
244
- }
245
- }
246
-
247
- /**
248
- * CATCHES: a rotation probe that is really a version check in disguise, or one
249
- * that reports UNKNOWN because it silently failed.
250
- *
251
- * The probe builds its own /Rotate 90 document and looks at where the ink
252
- * landed, so its answer is about the device rather than about
253
- * {@code Build.VERSION}. Asserting it is never UNKNOWN is what catches a probe
254
- * whose scratch write, parse or render failed — that failure mode is otherwise
255
- * indistinguishable from a genuinely unknown platform.
256
- */
257
- @Test
258
- public void rotationProbeAnswersAndCaches() {
259
- PdfPlatformQuirks.resetRotationSupportForTesting();
260
- PdfPlatformQuirks.RotationSupport first =
261
- PdfPlatformQuirks.rotationSupport(ctx().getCacheDir());
262
- PdfPlatformQuirks.RotationSupport second =
263
- PdfPlatformQuirks.rotationSupport(ctx().getCacheDir());
264
- Log.i(TAG, "quirk: API " + Build.VERSION.SDK_INT + " rotationSupport=" + first);
265
-
266
- assertNotEquals(
267
- "the probe should have reached a verdict, not failed silently",
268
- PdfPlatformQuirks.RotationSupport.UNKNOWN,
269
- first);
270
- assertEquals("the verdict must be cached, not re-probed differently", first, second);
271
-
272
- // The probe file must not be left behind in the cache directory.
273
- assertFalse(
274
- new File(ctx().getCacheDir(), "pdfcanvas-rotation-probe.pdf").exists());
275
- }
276
-
277
- /**
278
- * CATCHES: the rotation probe constructing a {@code PdfRenderer} on a process
279
- * whose PDFium is already poisoned.
280
- *
281
- * The probe was the ONE construction site that neither consulted the latch nor
282
- * armed it — it built a renderer inside a bare
283
- * {@code catch (Throwable) { return UNKNOWN; }}, so on an affected build it
284
- * was a segfault waiting for whichever screen asked about rotation first, and
285
- * a throw there was swallowed rather than latched. Refusing to probe is the
286
- * right answer because UNKNOWN is already a value callers must handle: the
287
- * enum's own comment says it is treated as unsupported.
288
- *
289
- * Asserting UNKNOWN is what makes this observable from outside. A probe that
290
- * ignored the latch would return SUPPORTED on API 31/33 and UNSUPPORTED on
291
- * API 24, so no single wrong constant passes on all three.
292
- */
293
- @Test
294
- public void theRotationProbeRefusesToRunOnAPoisonedProcess() {
295
- PdfPlatformQuirks.resetRotationSupportForTesting();
296
- try {
297
- PdfPlatformQuirks.setPdfiumPoisonedForTesting(true);
298
- assertEquals(
299
- "a poisoned process must not construct a PdfRenderer to answer a "
300
- + "rotation question",
301
- PdfPlatformQuirks.RotationSupport.UNKNOWN,
302
- PdfPlatformQuirks.rotationSupport(ctx().getCacheDir()));
303
- } finally {
304
- PdfPlatformQuirks.setPdfiumPoisonedForTesting(false);
305
- // The UNKNOWN above is now CACHED, and every other test that asks about
306
- // rotation would inherit it. Drop it so the next caller probes for real.
307
- PdfPlatformQuirks.resetRotationSupportForTesting();
308
- }
309
-
310
- // And the guard lifts: with the latch clear the probe reaches a real verdict.
311
- assertNotEquals(
312
- PdfPlatformQuirks.RotationSupport.UNKNOWN,
313
- PdfPlatformQuirks.rotationSupport(ctx().getCacheDir()));
314
- }
315
-
316
- private static PdfErrorCode openCode(File file) {
317
- try {
318
- PdfCanvasRasterizer.openFile(file, null, 1).close();
319
- return null;
320
- } catch (PdfRasterException e) {
321
- return e.code();
322
- }
323
- }
324
- }