• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LibreSign / libresign / 27454170133

13 Jun 2026 02:40AM UTC coverage: 56.884%. First build
27454170133

Pull #7740

github

web-flow
Merge 26d3ba702 into 55aab64af
Pull Request #7740: chore: migrate to PHP 8.3

23 of 35 new or added lines in 20 files covered. (65.71%)

10751 of 18900 relevant lines covered (56.88%)

7.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.77
/lib/Handler/DocMdpHandler.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
7
 * SPDX-License-Identifier: AGPL-3.0-or-later
8
 */
9

10
namespace OCA\Libresign\Handler;
11

12
use OCA\Libresign\Db\File;
13
use OCA\Libresign\Enum\DocMdpLevel;
14
use OCP\IL10N;
15

16
class DocMdpHandler {
17
        /** @var array<string, string[]> Allowed modification types per DocMDP level */
18
        private const array ALLOWED_MODIFICATIONS = [
19
                'CERTIFIED_NO_CHANGES_ALLOWED' => [],
20
                'CERTIFIED_FORM_FILLING' => ['form_field', 'template', 'signature'],
21
                'CERTIFIED_FORM_FILLING_AND_ANNOTATIONS' => ['form_field', 'template', 'annotation', 'signature'],
22
        ];
23

24
        public function __construct(
25
                private IL10N $l10n,
26
        ) {
27
        }
246✔
28

29
        public function allowsAdditionalSignatures($resource): bool {
30
                $docmdpLevel = $this->extractDocMdpLevel($resource);
44✔
31

32
                return $docmdpLevel !== DocMdpLevel::CERTIFIED_NO_CHANGES_ALLOWED;
44✔
33
        }
34

35
        public function extractDocMdpData($resource): array {
36
                if (!is_resource($resource)) {
33✔
37
                        return [];
×
38
                }
39

40
                rewind($resource);
33✔
41
                $content = stream_get_contents($resource);
33✔
42
                $isoStatus = $this->getIsoComplianceStatus($content);
33✔
43

44
                rewind($resource);
33✔
45
                $docmdpLevel = $this->extractDocMdpLevel($resource);
33✔
46

47
                $result = [
33✔
48
                        'docmdp' => [
33✔
49
                                'level' => $docmdpLevel->value,
33✔
50
                                'label' => $docmdpLevel->getLabel($this->l10n),
33✔
51
                                'description' => $docmdpLevel->getDescription($this->l10n),
33✔
52
                                'isCertifying' => $docmdpLevel->isCertifying(),
33✔
53
                        ],
33✔
54
                ];
33✔
55
                if ($docmdpLevel->isCertifying() && $this->isOnlySingleDocMdpViolation($isoStatus)) {
33✔
56
                        $result['docmdp_validation'] = [
2✔
57
                                'valid' => false,
2✔
58
                                'message' => $this->l10n->t('Multiple DocMDP signatures detected. The first certifying signature determines the document\'s permission level.'),
2✔
59
                        ];
2✔
60
                }
61

62
                $modificationInfo = $this->detectModifications($resource);
33✔
63
                $result['modifications'] = $modificationInfo;
33✔
64

65
                if ($modificationInfo['modified'] || $docmdpLevel->isCertifying()) {
33✔
66
                        $validation = $this->validateModifications($docmdpLevel, $modificationInfo, $resource);
24✔
67
                        $result['modification_validation'] = $validation;
24✔
68
                }
69

70
                return $result;
33✔
71
        }
72

73
        /**
74
         * Extract DocMDP permission level from PDF
75
         *
76
         * Validates ISO 32000-1 compliance:
77
         * - 12.8.2.2.1: Only ONE DocMDP signature allowed
78
         * - 12.8.2.2.1: DocMDP must be FIRST certifying signature
79
         * - Table 252: Signature dictionary validation (/Type /Sig, /Filter, /ByteRange)
80
         * - Table 253: Signature reference validation (/TransformMethod /DocMDP)
81
         * - Table 254: TransformParams validation (/P, /V /1.2)
82
         *
83
         * @return DocMdpLevel Permission level (NONE, NO_CHANGES, FORM_FILL, FORM_FILL_AND_ANNOTATIONS)
84
         */
85
        private function extractDocMdpLevel($pdfResource): DocMdpLevel {
86
                rewind($pdfResource);
76✔
87
                $content = stream_get_contents($pdfResource);
76✔
88

89
                $isoStatus = $this->getIsoComplianceStatus($content);
76✔
90
                if (!$this->isIsoCompliant($isoStatus) && !$this->isOnlySingleDocMdpViolation($isoStatus)) {
76✔
91
                        return DocMdpLevel::NOT_CERTIFIED;
36✔
92
                }
93

94
                $pValue = $this->extractPValue($content);
40✔
95
                if ($pValue === null) {
40✔
96
                        return DocMdpLevel::NOT_CERTIFIED;
3✔
97
                }
98

99
                return DocMdpLevel::tryFrom($pValue) ?? DocMdpLevel::NOT_CERTIFIED;
37✔
100
        }
101

102
        /**
103
         * Validate all ISO 32000-1 DocMDP requirements
104
         *
105
         * @return bool True if all validations pass
106
         */
107
        private function validateIsoCompliance(string $content): bool {
108
                return $this->validateSingleDocMdpSignature($content)
×
109
                        && $this->validateDocMdpIsFirstSignature($content)
×
110
                        && $this->validateSignatureDictionary($content)
×
111
                        && $this->validateSignatureReference($content);
×
112
        }
113

114
        private function getIsoComplianceStatus(string $content): array {
115
                return [
76✔
116
                        'single_docmdp' => $this->validateSingleDocMdpSignature($content),
76✔
117
                        'docmdp_first' => $this->validateDocMdpIsFirstSignature($content),
76✔
118
                        'sig_dict' => $this->validateSignatureDictionary($content),
76✔
119
                        'sig_ref' => $this->validateSignatureReference($content),
76✔
120
                ];
76✔
121
        }
122

123
        private function isIsoCompliant(array $status): bool {
124
                return $status['single_docmdp']
76✔
125
                        && $status['docmdp_first']
76✔
126
                        && $status['sig_dict']
76✔
127
                        && $status['sig_ref'];
76✔
128
        }
129

130
        private function isOnlySingleDocMdpViolation(array $status): bool {
131
                return !$status['single_docmdp']
57✔
132
                        && $status['docmdp_first']
57✔
133
                        && $status['sig_dict']
57✔
134
                        && $status['sig_ref'];
57✔
135
        }
136

137
        /**
138
         * Extract /P value from TransformParams (permission level)
139
         * ISO 32000-1 Table 254: /P is optional, default 2
140
         *
141
         * @return int|null Permission value (1, 2, or 3) or null if not found/invalid
142
         */
143
        private function extractPValue(string $content): ?int {
144
                if (preg_match('/\/Reference\s*\[\s*(\d+\s+\d+\s+R)/', $content, $refMatch)) {
40✔
145
                        $pValue = $this->extractPValueFromIndirectReference($content, $refMatch[1]);
4✔
146
                        if ($pValue !== null) {
4✔
147
                                return $pValue;
3✔
148
                        }
149
                }
150

151
                $inlinePattern = '/\/Reference\s*\[\s*<<.*?\/TransformMethod\s*\/DocMDP.*?\/TransformParams\s*<<.*?\/P\s*(\d+).*?>>.*?>>.*?\]/s';
37✔
152
                if (preg_match($inlinePattern, $content, $matches)) {
37✔
153
                        if ($this->validateTransformParamsVersion($content, $matches[0])) {
36✔
154
                                return (int)$matches[1];
34✔
155
                        }
156
                }
157

158
                return null;
3✔
159
        }
160

161
        /**
162
         * Extract /P value from indirect reference structure
163
         *
164
         * @param string $content Full PDF content
165
         * @param string $indirectRef Reference like "7 0 R"
166
         * @return int|null Permission value or null
167
         */
168
        private function extractPValueFromIndirectReference(string $content, string $indirectRef): ?int {
169
                $objPattern = '/' . preg_quote($indirectRef, '/') . '.*?obj\s*<<.*?\/TransformMethod\s*\/DocMDP.*?\/TransformParams\s*(\d+\s+\d+\s+R|<<.*?\/P\s*(\d+).*?>>)/s';
4✔
170

171
                if (!preg_match($objPattern, $content, $objMatch)) {
4✔
172
                        return null;
×
173
                }
174

175
                if (isset($objMatch[2]) && is_numeric($objMatch[2])) {
4✔
176
                        if ($this->validateTransformParamsVersion($content, $objMatch[0])) {
2✔
177
                                return (int)$objMatch[2];
2✔
178
                        }
179
                        return null;
×
180
                }
181

182
                if (isset($objMatch[1]) && preg_match('/(\d+\s+\d+\s+R)/', $objMatch[1], $paramsRef)) {
2✔
183
                        $objNum = preg_replace('/\s+R$/', '', $paramsRef[1]);
2✔
184
                        $paramsPattern = '/' . preg_quote((string)$objNum, '/') . '\s+obj\s*(<<.*?>>)\s*endobj/s';
2✔
185
                        if (preg_match($paramsPattern, $content, $paramsMatch)) {
2✔
186
                                if (preg_match('/\/P\s*(\d+)/', $paramsMatch[1], $pMatch)) {
2✔
187
                                        if ($this->validateTransformParamsVersion($content, $paramsMatch[0])) {
2✔
188
                                                return (int)$pMatch[1];
1✔
189
                                        }
190
                                }
191
                        }
192
                }
193

194
                return null;
1✔
195
        }
196

197
        /**
198
         * Parse all PDF objects (obj...endobj blocks) from content
199
         * Handles multi-line dictionaries with nested angle brackets
200
         *
201
         * @return array Array of objects with keys: objNum, dict, position
202
         */
203
        private function parsePdfObjects(string $content): array {
204
                if (!preg_match_all('/(\d+)\s+\d+\s+obj(.*?)endobj/s', $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
76✔
205
                        return [];
9✔
206
                }
207

208
                $objects = [];
67✔
209
                foreach ($matches as $match) {
67✔
210
                        $objects[] = [
67✔
211
                                'objNum' => $match[1][0],
67✔
212
                                'dict' => trim($match[2][0]),
67✔
213
                                'position' => $match[2][1],
67✔
214
                        ];
67✔
215
                }
216
                return $objects;
67✔
217
        }
218

219
        /**
220
         * ICP-Brasil DOC-ICP-15.03: Validate /V /1.2 in TransformParams
221
         * ISO 32000-1 Table 254: /V is optional, default 1.2
222
         */
223
        private function validateTransformParamsVersion(string $content, string $context): bool {
224
                if (preg_match('/\/TransformParams\s*(\d+\s+\d+\s+R)/', $context, $paramsRef)) {
40✔
225
                        $objNum = preg_replace('/\s+R$/', '', $paramsRef[1]);
×
NEW
226
                        $paramsPattern = '/' . preg_quote((string)$objNum, '/') . '\s+obj\s*(<<.*?>>)\s*endobj/s';
×
227
                        if (preg_match($paramsPattern, $content, $objMatch)) {
×
228
                                return preg_match('/\/V\s*\/1\.2/', $objMatch[1]) === 1;
×
229
                        }
230
                        return false;
×
231
                }
232
                return preg_match('/\/V\s*\/1\.2/', $context) === 1;
40✔
233
        }
234

235
        private function detectModifications($pdfResource): array {
236
                rewind($pdfResource);
33✔
237
                $content = stream_get_contents($pdfResource);
33✔
238
                $fileSize = strlen($content);
33✔
239

240
                preg_match_all(
33✔
241
                        '/ByteRange\s*\[\s*(?<offset1>\d+)\s+(?<length1>\d+)\s+(?<offset2>\d+)\s+(?<length2>\d+)\s*\]/',
33✔
242
                        $content,
33✔
243
                        $byteRanges,
33✔
244
                        PREG_SET_ORDER
33✔
245
                );
33✔
246

247
                if (empty($byteRanges)) {
33✔
248
                        return [
4✔
249
                                'modified' => false,
4✔
250
                                'revisionCount' => 0,
4✔
251
                                'details' => [],
4✔
252
                        ];
4✔
253
                }
254

255
                $modifications = [];
29✔
256
                foreach ($byteRanges as $index => $range) {
29✔
257
                        $coveredEnd = (int)$range['offset2'] + (int)$range['length2'];
29✔
258
                        $hasModifications = $coveredEnd < $fileSize;
29✔
259

260
                        $modifications[] = [
29✔
261
                                'signatureIndex' => $index,
29✔
262
                                'modified' => $hasModifications,
29✔
263
                                'coveredBytes' => $coveredEnd,
29✔
264
                                'totalBytes' => $fileSize,
29✔
265
                                'extraBytes' => $hasModifications ? ($fileSize - $coveredEnd) : 0,
29✔
266
                        ];
29✔
267
                }
268

269
                $isModified = array_reduce($modifications, fn ($carry, $item) => $carry || $item['modified'], false);
29✔
270

271
                return [
29✔
272
                        'modified' => $isModified,
29✔
273
                        'revisionCount' => count($byteRanges),
29✔
274
                        'details' => $modifications,
29✔
275
                ];
29✔
276
        }
277

278
        /**
279
         * Validate if modifications are allowed by DocMDP level
280
         * ISO 32000-1 Table 254: P=1 (no changes), P=2 (form fill), P=3 (form fill + annotations)
281
         *
282
         * @return array Validation result with keys: valid, status, message
283
         */
284
        private function validateModifications(DocMdpLevel $docmdpLevel, array $modificationInfo, $pdfResource): array {
285
                if (!$modificationInfo['modified']) {
24✔
286
                        return $this->buildValidationResult(
7✔
287
                                true,
7✔
288
                                File::MODIFICATION_UNMODIFIED,
7✔
289
                                'Document has not been modified after signing'
7✔
290
                        );
7✔
291
                }
292

293
                if ($docmdpLevel === DocMdpLevel::NOT_CERTIFIED) {
17✔
294
                        return $this->buildValidationResult(
3✔
295
                                true,
3✔
296
                                File::MODIFICATION_ALLOWED,
3✔
297
                                'Document was modified after signing'
3✔
298
                        );
3✔
299
                }
300

301
                $modificationType = $this->analyzeModificationType($pdfResource, $modificationInfo);
14✔
302
                $allowedTypes = self::ALLOWED_MODIFICATIONS[$docmdpLevel->name] ?? null;
14✔
303

304
                if ($allowedTypes === null) {
14✔
305
                        return $this->buildValidationResult(
×
306
                                false,
×
307
                                File::MODIFICATION_VIOLATION,
×
308
                                'Invalid: Document was modified after signing (DocMDP violation)'
×
309
                        );
×
310
                }
311

312
                $isAllowed = in_array($modificationType, $allowedTypes, true);
14✔
313

314
                return $isAllowed
14✔
315
                        ? $this->buildValidationResult(
9✔
316
                                true,
9✔
317
                                File::MODIFICATION_ALLOWED,
9✔
318
                                $this->getAllowedModificationMessage($docmdpLevel)
9✔
319
                        )
9✔
320
                        : $this->buildValidationResult(
14✔
321
                                false,
14✔
322
                                File::MODIFICATION_VIOLATION,
14✔
323
                                $this->getViolationMessage($docmdpLevel)
14✔
324
                        );
14✔
325
        }
326

327
        /**
328
         * Build validation result array
329
         *
330
         * @param bool $valid Whether modification is valid
331
         * @param int $status Status constant from File class
332
         * @param string $messageKey Translation key
333
         * @return array Validation result
334
         */
335
        private function buildValidationResult(bool $valid, int $status, string $messageKey): array {
336
                return [
24✔
337
                        'valid' => $valid,
24✔
338
                        'status' => $status,
24✔
339
                        'message' => $this->l10n->t($messageKey),
24✔
340
                ];
24✔
341
        }
342

343
        /**
344
         * Get success message for allowed modification
345
         *
346
         * @param DocMdpLevel $level DocMDP permission level
347
         * @return string Translated message
348
         */
349
        private function getAllowedModificationMessage(DocMdpLevel $level): string {
350
                return match ($level) {
351
                        DocMdpLevel::CERTIFIED_NO_CHANGES_ALLOWED => 'Invalid: Document was modified after signing (DocMDP violation - no changes allowed)',
9✔
352
                        DocMdpLevel::CERTIFIED_FORM_FILLING => 'Document form fields were modified (allowed by DocMDP P=2)',
9✔
353
                        DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => 'Document form fields or annotations were modified (allowed by DocMDP P=3)',
4✔
354
                        default => 'Document was modified after signing',
9✔
355
                };
356
        }
357

358
        /**
359
         * Get error message for modification violation
360
         *
361
         * @param DocMdpLevel $level DocMDP permission level
362
         * @return string Translated message
363
         */
364
        private function getViolationMessage(DocMdpLevel $level): string {
365
                return match ($level) {
366
                        DocMdpLevel::CERTIFIED_NO_CHANGES_ALLOWED => 'Invalid: Document was modified after signing (DocMDP violation - no changes allowed)',
5✔
367
                        DocMdpLevel::CERTIFIED_FORM_FILLING => 'Invalid: Document was modified after signing (DocMDP P=2 only allows form field changes)',
2✔
368
                        DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS => 'Invalid: Document was modified after signing (DocMDP P=3 only allows form fields and annotations)',
1✔
369
                        default => 'Invalid: Document was modified after signing (DocMDP violation)',
5✔
370
                };
371
        }
372

373
        /**
374
         * Analyze type of modification made to PDF after signing
375
         *
376
         * Patterns are checked in priority order (most specific first) to ensure
377
         * accurate classification when multiple patterns could match.
378
         *
379
         * @param resource $pdfResource PDF file resource
380
         * @param array $modificationInfo Modification detection info
381
         * @return string Modification type: signature, form_field, template, annotation, structural, unknown
382
         */
383
        private function analyzeModificationType($pdfResource, array $modificationInfo): string {
384
                if (empty($modificationInfo['details'])) {
14✔
385
                        return 'unknown';
×
386
                }
387

388
                rewind($pdfResource);
14✔
389
                $content = stream_get_contents($pdfResource);
14✔
390
                $coveredEnd = $modificationInfo['details'][0]['coveredBytes'];
14✔
391
                $modifiedContent = substr($content, $coveredEnd);
14✔
392

393
                $patterns = [
14✔
394
                        'signature' => '/\/Type\s*\/Sig/',
14✔
395
                        'form_field' => '/\/FT\s*\/(?:Tx|Ch|Btn)/',
14✔
396
                        'template' => '/\/Type\s*\/XObject\s*\/Subtype\s*\/Form/',
14✔
397
                        'annotation' => '/\/Type\s*\/Annot/',
14✔
398
                        'structural' => '/\/Type\s*\/Pages?/',
14✔
399
                ];
14✔
400

401
                foreach ($patterns as $type => $pattern) {
14✔
402
                        if (preg_match($pattern, $modifiedContent)) {
14✔
403
                                return $type;
14✔
404
                        }
405
                }
406

407
                return 'unknown';
×
408
        }
409

410
        /**
411
         * ISO 32000-1 12.8.2.2.1: A document can contain only one signature field that contains a DocMDP transform method
412
         */
413
        private function validateSingleDocMdpSignature(string $content): bool {
414
                $objects = $this->parsePdfObjects($content);
76✔
415
                if (empty($objects)) {
76✔
416
                        return false;
9✔
417
                }
418

419
                $latestByObj = [];
67✔
420
                foreach ($objects as $obj) {
67✔
421
                        $objNum = $obj['objNum'];
67✔
422
                        if (!isset($latestByObj[$objNum]) || $obj['position'] > $latestByObj[$objNum]['position']) {
67✔
423
                                $latestByObj[$objNum] = $obj;
67✔
424
                        }
425
                }
426

427
                $docMdpSignatureCount = 0;
67✔
428
                foreach ($latestByObj as $obj) {
67✔
429
                        $dict = $obj['dict'];
67✔
430
                        if (!preg_match('/\/Type\s*\/Sig\b/', (string)$dict) || !preg_match('/\/Reference\s*\[/', (string)$dict)) {
67✔
431
                                continue;
67✔
432
                        }
433
                        if ($this->signatureHasDocMdp($content, $dict)) {
44✔
434
                                $docMdpSignatureCount++;
43✔
435
                                if ($docMdpSignatureCount > 1) {
43✔
436
                                        return false;
2✔
437
                                }
438
                        }
439
                }
440

441
                // ISO 32000-1: exactly one DocMDP certifying signature is allowed.
442
                return $docMdpSignatureCount === 1;
65✔
443
        }
444

445
        /**
446
         * ISO 32000-1 12.8.2.2.1: DocMDP shall be the first signed field
447
         *
448
         * "First signed field" means first CERTIFYING signature (has /Reference)
449
         * that has been applied (/Contents present). Approval signatures (without
450
         * /Reference) don't count as they cannot have DocMDP.
451
         *
452
         * @return bool True if DocMDP is in first certifying signature
453
         */
454
        private function validateDocMdpIsFirstSignature(string $content): bool {
455
                $certifyingSignatures = $this->filterCertifyingSignatures($this->parsePdfObjects($content));
76✔
456

457
                if (empty($certifyingSignatures)) {
76✔
458
                        return false;
36✔
459
                }
460

461
                usort($certifyingSignatures, fn ($a, $b) => $a['position'] <=> $b['position']);
40✔
462

463
                return $this->signatureHasDocMdp($content, $certifyingSignatures[0]['dict']);
40✔
464
        }
465

466
        /**
467
         * Filter only certifying signatures from parsed objects
468
         *
469
         * @param array $objects Parsed PDF objects
470
         * @return array Certifying signatures with /Filter, /ByteRange, /Contents, /Reference
471
         */
472
        private function filterCertifyingSignatures(array $objects): array {
473
                return array_filter($objects, function ($obj) {
76✔
474
                        $dict = $obj['dict'];
67✔
475
                        return preg_match('/\/Filter\s*\//', $dict)
67✔
476
                                && preg_match('/\/ByteRange\s*\[/', $dict)
67✔
477
                                && preg_match('/\/Contents\s*</', $dict)
67✔
478
                                && preg_match('/\/Reference\s*\[/', $dict);
67✔
479
                });
76✔
480
        }
481

482
        /**
483
         * Check if signature dictionary has DocMDP (inline or indirect)
484
         *
485
         * @param string $content Full PDF content
486
         * @param string $dict Signature dictionary
487
         * @return bool True if has DocMDP
488
         */
489
        private function signatureHasDocMdp(string $content, string $dict): bool {
490
                if (preg_match('/\/Reference\s*\[.*?\/TransformMethod\s*\/DocMDP/s', $dict)) {
44✔
491
                        return true;
38✔
492
                }
493

494
                if (preg_match('/\/Reference\s*\[\s*(\d+)\s+\d+\s+R/', $dict, $refMatch)) {
6✔
495
                        $refPattern = '/' . $refMatch[1] . '\s+\d+\s+obj\s*<<.*?\/TransformMethod\s*\/DocMDP.*?>>.*?endobj/s';
5✔
496
                        return (bool)preg_match($refPattern, $content);
5✔
497
                }
498

499
                return false;
1✔
500
        }
501

502
        /**
503
         * ISO 32000-1 Table 252: Validate signature dictionary entries
504
         *
505
         * Required entries:
506
         * - /Type /Sig (optional, but if present must be /Sig)
507
         * - /Filter (Required) - signature handler name
508
         * - /ByteRange (Required for DocMDP) - byte ranges covered by signature
509
         *
510
         * @return bool True if signature dictionary is valid
511
         */
512
        private function validateSignatureDictionary(string $content): bool {
513
                $objects = $this->parsePdfObjects($content);
76✔
514
                $sigDict = $this->findSignatureDictionary($objects);
76✔
515

516
                if (!$sigDict) {
76✔
517
                        return false;
32✔
518
                }
519

520
                return $this->validateDictionaryEntries($sigDict);
44✔
521
        }
522

523
        private function findSignatureDictionary(array $objects): ?string {
524
                foreach ($objects as $obj) {
76✔
525
                        $dict = $obj['dict'];
67✔
526
                        if (preg_match('/\/Type\s*\/Sig\b/', (string)$dict) && preg_match('/\/Reference\s*\[/', (string)$dict)) {
67✔
527
                                return $dict;
44✔
528
                        }
529
                }
530
                return null;
32✔
531
        }
532

533
        /**
534
         * Validate signature dictionary entries per ISO Table 252
535
         *
536
         * @param string $dict Dictionary content
537
         * @return bool True if all required entries are valid
538
         */
539
        private function validateDictionaryEntries(string $dict): bool {
540
                if (!preg_match('/\/Type\s*\/Sig\b/', $dict)) {
44✔
541
                        return false;
×
542
                }
543

544
                if (!preg_match('/\/Filter\s*\/[\w.]+/', $dict)) {
44✔
545
                        return false;
1✔
546
                }
547

548
                return (bool)preg_match('/\/ByteRange\s*\[/', $dict);
43✔
549
        }
550

551
        /**
552
         * ISO 32000-1 Table 253: Validate signature reference dictionary
553
         *
554
         * @return bool True if /TransformMethod /DocMDP is present (inline or indirect)
555
         */
556
        private function validateSignatureReference(string $content): bool {
557
                return (bool)preg_match('/\/TransformMethod\s*\/DocMDP/', $content);
76✔
558
        }
559
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc