From 42bdc57b958d0af42a130c486275350ccdd32a37 Mon Sep 17 00:00:00 2001 From: Brad Haas Date: Tue, 14 Apr 2026 20:59:06 -0400 Subject: [PATCH 1/3] feat: replace x509-parser with in-tree minimal DER parser (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #22. Replaces the ~28-crate x509-parser dependency tree with ~1500 lines of strict-DER, no-unsafe, panic-free in-tree parser code scoped to exactly what certinfo exposes to Python. Final Rust dep tree shrinks from 48 crates to 20 — every remaining crate is pyo3 or a pyo3 build-time helper. Module structure (modern Rust 2018+ layout, no mod.rs): rust_certinfo/src/ lib.rs PyO3 module + thin entry-point shim error.rs ParseError enum (no panics) pem.rs Inlined RFC 4648 b64 + PEM wrap pyobj.rs PyO3 dict converters der.rs / der/ ASN.1 primitive layer (reader, oid, time, string, tag) — knows nothing about X.509 x509.rs / x509/ X.509 layer (certificate, name, spki, algorithm, extensions) — built on der/ The DER primitive layer is a clean reusable foundation for future X.509-adjacent capabilities: SAN parsing for non-leaf certs, authorityInfoAccess (OCSP/AIA URLs), cRLDistributionPoints, extendedKeyUsage / keyUsage, certificate policies for EV detection, CRL parsing, OCSP request/response parsing, CSR parsing. Each is a matter of adding a single function under x509/ — the der/ layer requires no changes for any of the above. Strict-DER security guarantees, all enforced at the crate level: * #![forbid(unsafe_code)] at lib.rs root * Every parser path returns Result<_, ParseError> — no panics on malformed input * Reject indefinite-length encoding (BER-only, illegal in DER) * Reject non-canonical length encoding (long form when short would suffice; long form with leading zero bytes) * Bounds checks against the parent slice on every read * 56 in-module Rust unit tests covering every public function, every ParseError construction path, and DER edge cases In addition to dropping x509-parser, this PR fixes two latent bugs discovered during the rewrite: * EC `curve` field now contains the curve OID. Previous builds put the algorithm OID `1.2.840.10045.2.1` (id-ecPublicKey) into the field literally named `curve`. The new parser extracts the curve OID from algorithm.parameters and emits e.g. `1.2.840.10045.3.1.7` for P-256, `1.3.132.0.34` for P-384, `1.3.132.0.35` for P-521. * RSA modulus bit length is no longer over-counted by 8 bits. The previous build computed `modulus.len() * 8` from x509-parser, which leaves the DER-mandated leading-zero sign byte in modulus. Real-world RSA-2048 / 3072 / 4096 keys were being reported as 2056 / 3080 / 4104. The new parser strips the sign byte and reports the canonical 2048 / 3072 / 4096. Both fixes are visible behavioral changes for any caller reading `public_key_info["curve"]` or `public_key_info["size"]` literally. Test coverage: * 425 Python tests passing (was 407 — 18 added in the new corpus snapshot test), 99% line coverage * 56 Rust unit tests passing * tests/test_certinfo_corpus.py — new snapshot test that runs every public certinfo entry point against 130 unique real-world certs captured from the bench host list. Asserts RSA bit lengths are canonical, EC curves resolve to real curve OIDs (catches the fixed bug as a regression), validity timestamps are sane, all DNs decode, and SPKI extraction round-trips. * tests/fixtures/diff_corpus/ — 130 captured DER fixtures from 101 stable public hosts spanning Google Trust Services, DigiCert, Let's Encrypt, Sectigo, ISRG, SSL.com, Cloudflare-fronted certs, etc. Roughly 50/50 RSA/EC. Manual fuzzing gate at rust_certinfo/fuzz/ (cargo fuzz target + README). Not in CI (nightly + slow); release-time pre-merge gate. Other changes: * Cargo.toml crate-type now declares ["cdylib", "rlib"]. The cdylib is the same Python wheel maturin has always built; the additional rlib lets the in-repo fuzz crate link against the parser as a normal Rust library. No published-wheel surface change. * certinfo::Certificate::from_der and certinfo::ParseError are now pub at the crate root for the fuzz crate and any future in-tree Rust consumer. The PyO3 boundary and Python-facing API are unchanged. Closes #22 --- CHANGELOG.md | 10 +- Cargo.lock | 271 ------------ Cargo.toml | 10 +- MODULARIZATION_REPORT.md | 8 +- rust_certinfo/fuzz/Cargo.toml | 29 ++ rust_certinfo/fuzz/README.md | 54 +++ .../fuzz/fuzz_targets/parse_certificate.rs | 20 + rust_certinfo/src/der.rs | 14 + rust_certinfo/src/der/oid.rs | 204 +++++++++ rust_certinfo/src/der/reader.rs | 215 +++++++++ rust_certinfo/src/der/string.rs | 163 +++++++ rust_certinfo/src/der/tag.rs | 33 ++ rust_certinfo/src/der/time.rs | 208 +++++++++ rust_certinfo/src/error.rs | 64 +++ rust_certinfo/src/lib.rs | 415 ++---------------- rust_certinfo/src/pem.rs | 110 +++++ rust_certinfo/src/pyobj.rs | 229 ++++++++++ rust_certinfo/src/x509.rs | 17 + rust_certinfo/src/x509/algorithm.rs | 101 +++++ rust_certinfo/src/x509/certificate.rs | 132 ++++++ rust_certinfo/src/x509/extensions.rs | 273 ++++++++++++ rust_certinfo/src/x509/name.rs | 255 +++++++++++ rust_certinfo/src/x509/spki.rs | 281 ++++++++++++ .../fixtures/diff_corpus/01e4587189b66739.der | Bin 0 -> 1930 bytes .../fixtures/diff_corpus/04183fea3b7dbc23.der | Bin 0 -> 1916 bytes .../fixtures/diff_corpus/0587d6bd2819587a.der | Bin 0 -> 893 bytes .../fixtures/diff_corpus/077c81a10ee1096d.der | Bin 0 -> 953 bytes .../fixtures/diff_corpus/0780fe78243e6b0c.der | Bin 0 -> 2082 bytes .../fixtures/diff_corpus/092ac16e8faed8a1.der | Bin 0 -> 1640 bytes .../fixtures/diff_corpus/0a05f2e1bec8c474.der | Bin 0 -> 922 bytes .../fixtures/diff_corpus/0dab0ae8be64366a.der | Bin 0 -> 964 bytes .../fixtures/diff_corpus/0e82620b9e99e2d6.der | Bin 0 -> 1008 bytes .../fixtures/diff_corpus/131fce7784016899.der | Bin 0 -> 1290 bytes .../fixtures/diff_corpus/138bdf6e23ac971e.der | Bin 0 -> 1122 bytes .../fixtures/diff_corpus/15581c41023f0789.der | Bin 0 -> 1666 bytes .../fixtures/diff_corpus/179fbc148a3dd00f.der | Bin 0 -> 546 bytes .../fixtures/diff_corpus/1af627c6c2ac992e.der | Bin 0 -> 982 bytes .../fixtures/diff_corpus/1be011f72535b418.der | Bin 0 -> 1566 bytes .../fixtures/diff_corpus/1dfc1605fbad358d.der | Bin 0 -> 675 bytes .../fixtures/diff_corpus/1f8eb9e9a8e066cc.der | Bin 0 -> 1272 bytes .../fixtures/diff_corpus/2585928d2c5bfd95.der | Bin 0 -> 950 bytes .../fixtures/diff_corpus/27858fbe414ac907.der | Bin 0 -> 1276 bytes .../fixtures/diff_corpus/2964fd3210ea68fa.der | Bin 0 -> 744 bytes .../fixtures/diff_corpus/2a317f1f5d8267af.der | Bin 0 -> 1308 bytes .../fixtures/diff_corpus/2a575471e31340bc.der | Bin 0 -> 1374 bytes .../fixtures/diff_corpus/2ac5352a4c603fff.der | Bin 0 -> 1480 bytes .../fixtures/diff_corpus/2af988f26f6ef0da.der | Bin 0 -> 817 bytes .../fixtures/diff_corpus/2ce1cb0bf9d2f9e1.der | Bin 0 -> 993 bytes .../fixtures/diff_corpus/2d74167e675e0223.der | Bin 0 -> 1497 bytes .../fixtures/diff_corpus/2f843e8a48ae698e.der | Bin 0 -> 937 bytes .../fixtures/diff_corpus/2fe357db13751ff9.der | Bin 0 -> 1295 bytes .../fixtures/diff_corpus/31437de9c9f2631d.der | Bin 0 -> 1406 bytes .../fixtures/diff_corpus/31ad6648f8104138.der | Bin 0 -> 579 bytes .../fixtures/diff_corpus/340ca5ba402d140b.der | Bin 0 -> 1314 bytes .../fixtures/diff_corpus/394e114f9601996c.der | Bin 0 -> 2168 bytes .../fixtures/diff_corpus/3d50ee2e469b26d1.der | Bin 0 -> 1344 bytes .../fixtures/diff_corpus/3e8655b1b920e871.der | Bin 0 -> 1713 bytes .../fixtures/diff_corpus/3fcb4dc9da35fe86.der | Bin 0 -> 1173 bytes .../fixtures/diff_corpus/4026117dc787e06a.der | Bin 0 -> 1756 bytes .../fixtures/diff_corpus/45140b3247eb9cc8.der | Bin 0 -> 969 bytes .../fixtures/diff_corpus/45d045616ee8db33.der | Bin 0 -> 1172 bytes .../fixtures/diff_corpus/4720a86f440eb48c.der | Bin 0 -> 1633 bytes .../fixtures/diff_corpus/48bcbb098eef9326.der | Bin 0 -> 913 bytes .../fixtures/diff_corpus/4a39d89154af2f63.der | Bin 0 -> 2380 bytes .../fixtures/diff_corpus/4bc352e324456ac0.der | Bin 0 -> 918 bytes .../fixtures/diff_corpus/4bcc5e234fe81ede.der | Bin 0 -> 1167 bytes .../fixtures/diff_corpus/4ff460d54b9c86da.der | Bin 0 -> 659 bytes .../fixtures/diff_corpus/50f6e40f406a9583.der | Bin 0 -> 2755 bytes .../fixtures/diff_corpus/5132b2bca3f1bc9c.der | Bin 0 -> 1134 bytes .../fixtures/diff_corpus/5186c5ec3d3e7b93.der | Bin 0 -> 1681 bytes .../fixtures/diff_corpus/5338ebec8fb2ac60.der | Bin 0 -> 1122 bytes .../fixtures/diff_corpus/5539f8c901051834.der | Bin 0 -> 1172 bytes .../fixtures/diff_corpus/5602edbbca1da24e.der | Bin 0 -> 1634 bytes .../fixtures/diff_corpus/5d1bc399274e649e.der | Bin 0 -> 824 bytes .../fixtures/diff_corpus/5dbeb4d447838c7e.der | Bin 0 -> 1535 bytes .../fixtures/diff_corpus/6542d176bed50f19.der | Bin 0 -> 1616 bytes .../fixtures/diff_corpus/66ee1b77867ac881.der | Bin 0 -> 953 bytes .../fixtures/diff_corpus/71cca5391f9e794b.der | Bin 0 -> 526 bytes .../fixtures/diff_corpus/7201f2fb02d26d20.der | Bin 0 -> 918 bytes .../fixtures/diff_corpus/750a1e95de398a7b.der | Bin 0 -> 2384 bytes .../fixtures/diff_corpus/7dd15e69e76b9942.der | Bin 0 -> 933 bytes .../fixtures/diff_corpus/7e2fa4d1f1694fb3.der | Bin 0 -> 1781 bytes .../fixtures/diff_corpus/7f40b0f59d88f92e.der | Bin 0 -> 3539 bytes .../fixtures/diff_corpus/7fa4ff68ec04a99d.der | Bin 0 -> 1559 bytes .../fixtures/diff_corpus/800b82bf98a4a76c.der | Bin 0 -> 1291 bytes .../fixtures/diff_corpus/83624fd338c8d9b0.der | Bin 0 -> 1114 bytes .../fixtures/diff_corpus/873f0ba80e3ac222.der | Bin 0 -> 867 bytes .../fixtures/diff_corpus/87a4d12db9b57d68.der | Bin 0 -> 2040 bytes .../fixtures/diff_corpus/87c71553445eb3c3.der | Bin 0 -> 774 bytes .../fixtures/diff_corpus/8a709990bf0363e3.der | Bin 0 -> 1515 bytes .../fixtures/diff_corpus/8c54c334b66ba4e4.der | Bin 0 -> 1616 bytes .../fixtures/diff_corpus/8eb2f17d668941c3.der | Bin 0 -> 1599 bytes .../fixtures/diff_corpus/8ecde6884f3d87b1.der | Bin 0 -> 837 bytes .../fixtures/diff_corpus/8f14bd119660d75f.der | Bin 0 -> 2454 bytes .../fixtures/diff_corpus/8fac576439c9fd3e.der | Bin 0 -> 1167 bytes .../fixtures/diff_corpus/90500fe49040b550.der | Bin 0 -> 1668 bytes .../fixtures/diff_corpus/92f351bf3d54164d.der | Bin 0 -> 1689 bytes .../fixtures/diff_corpus/9588ef74199e45ac.der | Bin 0 -> 1344 bytes .../fixtures/diff_corpus/96a277a357ae2fb8.der | Bin 0 -> 933 bytes .../fixtures/diff_corpus/96bcec06264976f3.der | Bin 0 -> 1391 bytes .../fixtures/diff_corpus/9716d39441ca651c.der | Bin 0 -> 1007 bytes .../fixtures/diff_corpus/973a41276ffd01e0.der | Bin 0 -> 1236 bytes .../fixtures/diff_corpus/98c2692704108e8b.der | Bin 0 -> 921 bytes .../fixtures/diff_corpus/9b46403b73e9c9e7.der | Bin 0 -> 1277 bytes .../fixtures/diff_corpus/9d1bc5d2dd75bf8b.der | Bin 0 -> 1456 bytes .../fixtures/diff_corpus/9f17b548f7d24f30.der | Bin 0 -> 2478 bytes .../fixtures/diff_corpus/a162964cfe4209e3.der | Bin 0 -> 1670 bytes .../fixtures/diff_corpus/a75dcf6b0c306aa0.der | Bin 0 -> 1670 bytes .../fixtures/diff_corpus/ab1fe26963731abd.der | Bin 0 -> 1991 bytes .../fixtures/diff_corpus/ac8ea9f2874fd368.der | Bin 0 -> 1980 bytes .../fixtures/diff_corpus/accbf2b1744959f9.der | Bin 0 -> 1272 bytes .../fixtures/diff_corpus/ade92438c82b13f3.der | Bin 0 -> 4991 bytes .../fixtures/diff_corpus/aeb1fd7410e83bc9.der | Bin 0 -> 1115 bytes .../fixtures/diff_corpus/aef9144c2c95e81f.der | Bin 0 -> 1255 bytes .../fixtures/diff_corpus/b09f6b59d29b3ecf.der | Bin 0 -> 913 bytes .../fixtures/diff_corpus/b0f330a31a0c5098.der | Bin 0 -> 1122 bytes .../fixtures/diff_corpus/b4ce1dc88a2bbfd8.der | Bin 0 -> 1316 bytes .../fixtures/diff_corpus/b5fcf79e5bb2688f.der | Bin 0 -> 1632 bytes .../fixtures/diff_corpus/b63e5b2303d862a7.der | Bin 0 -> 914 bytes .../fixtures/diff_corpus/b676ffa3179e8812.der | Bin 0 -> 1106 bytes .../fixtures/diff_corpus/b85acbe6605ec61f.der | Bin 0 -> 928 bytes .../fixtures/diff_corpus/ba06d3d3e348fce7.der | Bin 0 -> 1090 bytes .../fixtures/diff_corpus/c280828b6bf22be2.der | Bin 0 -> 1779 bytes .../fixtures/diff_corpus/c68631bd6387e733.der | Bin 0 -> 1578 bytes .../fixtures/diff_corpus/c8025f9fc65fdfc9.der | Bin 0 -> 1228 bytes .../fixtures/diff_corpus/c93e5b7b18fdfa6d.der | Bin 0 -> 3479 bytes .../fixtures/diff_corpus/c9421441d1109cf5.der | Bin 0 -> 1584 bytes .../fixtures/diff_corpus/cb3ccbb76031e5e0.der | Bin 0 -> 914 bytes .../fixtures/diff_corpus/cbb522d7b7f127ad.der | Bin 0 -> 867 bytes .../fixtures/diff_corpus/cdbff9bd761f2283.der | Bin 0 -> 1283 bytes .../fixtures/diff_corpus/ce76b85cebc25f5f.der | Bin 0 -> 2305 bytes .../fixtures/diff_corpus/cfa6cb614dbd503d.der | Bin 0 -> 1620 bytes .../fixtures/diff_corpus/cfce1866e0240d03.der | Bin 0 -> 2137 bytes .../fixtures/diff_corpus/d223496393f995e9.der | Bin 0 -> 3642 bytes .../fixtures/diff_corpus/d259c071b22ee845.der | Bin 0 -> 1731 bytes .../fixtures/diff_corpus/d3b128216a843f8e.der | Bin 0 -> 1289 bytes .../fixtures/diff_corpus/d43d26e6702b9c14.der | Bin 0 -> 1570 bytes .../fixtures/diff_corpus/d7a7a0fb5d7e2731.der | Bin 0 -> 1078 bytes .../fixtures/diff_corpus/da9fca34e821865e.der | Bin 0 -> 1020 bytes .../fixtures/diff_corpus/ddcd1e8a20638d4a.der | Bin 0 -> 1421 bytes .../fixtures/diff_corpus/e08f679765df94c4.der | Bin 0 -> 1116 bytes .../fixtures/diff_corpus/e13650ac25e75323.der | Bin 0 -> 6404 bytes .../fixtures/diff_corpus/e339a6843b0f4890.der | Bin 0 -> 936 bytes .../fixtures/diff_corpus/e6fe22bf45e4f0d3.der | Bin 0 -> 1295 bytes .../fixtures/diff_corpus/e793c9b02fd8aa13.der | Bin 0 -> 1506 bytes .../fixtures/diff_corpus/ea69bc711cb9d456.der | Bin 0 -> 1352 bytes .../fixtures/diff_corpus/ea6b89ed6907a209.der | Bin 0 -> 842 bytes .../fixtures/diff_corpus/ea7a25255d111fc3.der | Bin 0 -> 1980 bytes .../fixtures/diff_corpus/ec5c5d684fd5c4f5.der | Bin 0 -> 1707 bytes .../fixtures/diff_corpus/ed13bd4d797ad34f.der | Bin 0 -> 2620 bytes .../fixtures/diff_corpus/f5165fc624453361.der | Bin 0 -> 1172 bytes .../fixtures/diff_corpus/fe949f2d842955cc.der | Bin 0 -> 1410 bytes .../fixtures/diff_corpus/fec41e32ca75c295.der | Bin 0 -> 1188 bytes tests/test_certinfo_corpus.py | 241 ++++++++++ 154 files changed, 2709 insertions(+), 648 deletions(-) create mode 100644 rust_certinfo/fuzz/Cargo.toml create mode 100644 rust_certinfo/fuzz/README.md create mode 100644 rust_certinfo/fuzz/fuzz_targets/parse_certificate.rs create mode 100644 rust_certinfo/src/der.rs create mode 100644 rust_certinfo/src/der/oid.rs create mode 100644 rust_certinfo/src/der/reader.rs create mode 100644 rust_certinfo/src/der/string.rs create mode 100644 rust_certinfo/src/der/tag.rs create mode 100644 rust_certinfo/src/der/time.rs create mode 100644 rust_certinfo/src/error.rs create mode 100644 rust_certinfo/src/pem.rs create mode 100644 rust_certinfo/src/pyobj.rs create mode 100644 rust_certinfo/src/x509.rs create mode 100644 rust_certinfo/src/x509/algorithm.rs create mode 100644 rust_certinfo/src/x509/certificate.rs create mode 100644 rust_certinfo/src/x509/extensions.rs create mode 100644 rust_certinfo/src/x509/name.rs create mode 100644 rust_certinfo/src/x509/spki.rs create mode 100644 tests/fixtures/diff_corpus/01e4587189b66739.der create mode 100644 tests/fixtures/diff_corpus/04183fea3b7dbc23.der create mode 100644 tests/fixtures/diff_corpus/0587d6bd2819587a.der create mode 100644 tests/fixtures/diff_corpus/077c81a10ee1096d.der create mode 100644 tests/fixtures/diff_corpus/0780fe78243e6b0c.der create mode 100644 tests/fixtures/diff_corpus/092ac16e8faed8a1.der create mode 100644 tests/fixtures/diff_corpus/0a05f2e1bec8c474.der create mode 100644 tests/fixtures/diff_corpus/0dab0ae8be64366a.der create mode 100644 tests/fixtures/diff_corpus/0e82620b9e99e2d6.der create mode 100644 tests/fixtures/diff_corpus/131fce7784016899.der create mode 100644 tests/fixtures/diff_corpus/138bdf6e23ac971e.der create mode 100644 tests/fixtures/diff_corpus/15581c41023f0789.der create mode 100644 tests/fixtures/diff_corpus/179fbc148a3dd00f.der create mode 100644 tests/fixtures/diff_corpus/1af627c6c2ac992e.der create mode 100644 tests/fixtures/diff_corpus/1be011f72535b418.der create mode 100644 tests/fixtures/diff_corpus/1dfc1605fbad358d.der create mode 100644 tests/fixtures/diff_corpus/1f8eb9e9a8e066cc.der create mode 100644 tests/fixtures/diff_corpus/2585928d2c5bfd95.der create mode 100644 tests/fixtures/diff_corpus/27858fbe414ac907.der create mode 100644 tests/fixtures/diff_corpus/2964fd3210ea68fa.der create mode 100644 tests/fixtures/diff_corpus/2a317f1f5d8267af.der create mode 100644 tests/fixtures/diff_corpus/2a575471e31340bc.der create mode 100644 tests/fixtures/diff_corpus/2ac5352a4c603fff.der create mode 100644 tests/fixtures/diff_corpus/2af988f26f6ef0da.der create mode 100644 tests/fixtures/diff_corpus/2ce1cb0bf9d2f9e1.der create mode 100644 tests/fixtures/diff_corpus/2d74167e675e0223.der create mode 100644 tests/fixtures/diff_corpus/2f843e8a48ae698e.der create mode 100644 tests/fixtures/diff_corpus/2fe357db13751ff9.der create mode 100644 tests/fixtures/diff_corpus/31437de9c9f2631d.der create mode 100644 tests/fixtures/diff_corpus/31ad6648f8104138.der create mode 100644 tests/fixtures/diff_corpus/340ca5ba402d140b.der create mode 100644 tests/fixtures/diff_corpus/394e114f9601996c.der create mode 100644 tests/fixtures/diff_corpus/3d50ee2e469b26d1.der create mode 100644 tests/fixtures/diff_corpus/3e8655b1b920e871.der create mode 100644 tests/fixtures/diff_corpus/3fcb4dc9da35fe86.der create mode 100644 tests/fixtures/diff_corpus/4026117dc787e06a.der create mode 100644 tests/fixtures/diff_corpus/45140b3247eb9cc8.der create mode 100644 tests/fixtures/diff_corpus/45d045616ee8db33.der create mode 100644 tests/fixtures/diff_corpus/4720a86f440eb48c.der create mode 100644 tests/fixtures/diff_corpus/48bcbb098eef9326.der create mode 100644 tests/fixtures/diff_corpus/4a39d89154af2f63.der create mode 100644 tests/fixtures/diff_corpus/4bc352e324456ac0.der create mode 100644 tests/fixtures/diff_corpus/4bcc5e234fe81ede.der create mode 100644 tests/fixtures/diff_corpus/4ff460d54b9c86da.der create mode 100644 tests/fixtures/diff_corpus/50f6e40f406a9583.der create mode 100644 tests/fixtures/diff_corpus/5132b2bca3f1bc9c.der create mode 100644 tests/fixtures/diff_corpus/5186c5ec3d3e7b93.der create mode 100644 tests/fixtures/diff_corpus/5338ebec8fb2ac60.der create mode 100644 tests/fixtures/diff_corpus/5539f8c901051834.der create mode 100644 tests/fixtures/diff_corpus/5602edbbca1da24e.der create mode 100644 tests/fixtures/diff_corpus/5d1bc399274e649e.der create mode 100644 tests/fixtures/diff_corpus/5dbeb4d447838c7e.der create mode 100644 tests/fixtures/diff_corpus/6542d176bed50f19.der create mode 100644 tests/fixtures/diff_corpus/66ee1b77867ac881.der create mode 100644 tests/fixtures/diff_corpus/71cca5391f9e794b.der create mode 100644 tests/fixtures/diff_corpus/7201f2fb02d26d20.der create mode 100644 tests/fixtures/diff_corpus/750a1e95de398a7b.der create mode 100644 tests/fixtures/diff_corpus/7dd15e69e76b9942.der create mode 100644 tests/fixtures/diff_corpus/7e2fa4d1f1694fb3.der create mode 100644 tests/fixtures/diff_corpus/7f40b0f59d88f92e.der create mode 100644 tests/fixtures/diff_corpus/7fa4ff68ec04a99d.der create mode 100644 tests/fixtures/diff_corpus/800b82bf98a4a76c.der create mode 100644 tests/fixtures/diff_corpus/83624fd338c8d9b0.der create mode 100644 tests/fixtures/diff_corpus/873f0ba80e3ac222.der create mode 100644 tests/fixtures/diff_corpus/87a4d12db9b57d68.der create mode 100644 tests/fixtures/diff_corpus/87c71553445eb3c3.der create mode 100644 tests/fixtures/diff_corpus/8a709990bf0363e3.der create mode 100644 tests/fixtures/diff_corpus/8c54c334b66ba4e4.der create mode 100644 tests/fixtures/diff_corpus/8eb2f17d668941c3.der create mode 100644 tests/fixtures/diff_corpus/8ecde6884f3d87b1.der create mode 100644 tests/fixtures/diff_corpus/8f14bd119660d75f.der create mode 100644 tests/fixtures/diff_corpus/8fac576439c9fd3e.der create mode 100644 tests/fixtures/diff_corpus/90500fe49040b550.der create mode 100644 tests/fixtures/diff_corpus/92f351bf3d54164d.der create mode 100644 tests/fixtures/diff_corpus/9588ef74199e45ac.der create mode 100644 tests/fixtures/diff_corpus/96a277a357ae2fb8.der create mode 100644 tests/fixtures/diff_corpus/96bcec06264976f3.der create mode 100644 tests/fixtures/diff_corpus/9716d39441ca651c.der create mode 100644 tests/fixtures/diff_corpus/973a41276ffd01e0.der create mode 100644 tests/fixtures/diff_corpus/98c2692704108e8b.der create mode 100644 tests/fixtures/diff_corpus/9b46403b73e9c9e7.der create mode 100644 tests/fixtures/diff_corpus/9d1bc5d2dd75bf8b.der create mode 100644 tests/fixtures/diff_corpus/9f17b548f7d24f30.der create mode 100644 tests/fixtures/diff_corpus/a162964cfe4209e3.der create mode 100644 tests/fixtures/diff_corpus/a75dcf6b0c306aa0.der create mode 100644 tests/fixtures/diff_corpus/ab1fe26963731abd.der create mode 100644 tests/fixtures/diff_corpus/ac8ea9f2874fd368.der create mode 100644 tests/fixtures/diff_corpus/accbf2b1744959f9.der create mode 100644 tests/fixtures/diff_corpus/ade92438c82b13f3.der create mode 100644 tests/fixtures/diff_corpus/aeb1fd7410e83bc9.der create mode 100644 tests/fixtures/diff_corpus/aef9144c2c95e81f.der create mode 100644 tests/fixtures/diff_corpus/b09f6b59d29b3ecf.der create mode 100644 tests/fixtures/diff_corpus/b0f330a31a0c5098.der create mode 100644 tests/fixtures/diff_corpus/b4ce1dc88a2bbfd8.der create mode 100644 tests/fixtures/diff_corpus/b5fcf79e5bb2688f.der create mode 100644 tests/fixtures/diff_corpus/b63e5b2303d862a7.der create mode 100644 tests/fixtures/diff_corpus/b676ffa3179e8812.der create mode 100644 tests/fixtures/diff_corpus/b85acbe6605ec61f.der create mode 100644 tests/fixtures/diff_corpus/ba06d3d3e348fce7.der create mode 100644 tests/fixtures/diff_corpus/c280828b6bf22be2.der create mode 100644 tests/fixtures/diff_corpus/c68631bd6387e733.der create mode 100644 tests/fixtures/diff_corpus/c8025f9fc65fdfc9.der create mode 100644 tests/fixtures/diff_corpus/c93e5b7b18fdfa6d.der create mode 100644 tests/fixtures/diff_corpus/c9421441d1109cf5.der create mode 100644 tests/fixtures/diff_corpus/cb3ccbb76031e5e0.der create mode 100644 tests/fixtures/diff_corpus/cbb522d7b7f127ad.der create mode 100644 tests/fixtures/diff_corpus/cdbff9bd761f2283.der create mode 100644 tests/fixtures/diff_corpus/ce76b85cebc25f5f.der create mode 100644 tests/fixtures/diff_corpus/cfa6cb614dbd503d.der create mode 100644 tests/fixtures/diff_corpus/cfce1866e0240d03.der create mode 100644 tests/fixtures/diff_corpus/d223496393f995e9.der create mode 100644 tests/fixtures/diff_corpus/d259c071b22ee845.der create mode 100644 tests/fixtures/diff_corpus/d3b128216a843f8e.der create mode 100644 tests/fixtures/diff_corpus/d43d26e6702b9c14.der create mode 100644 tests/fixtures/diff_corpus/d7a7a0fb5d7e2731.der create mode 100644 tests/fixtures/diff_corpus/da9fca34e821865e.der create mode 100644 tests/fixtures/diff_corpus/ddcd1e8a20638d4a.der create mode 100644 tests/fixtures/diff_corpus/e08f679765df94c4.der create mode 100644 tests/fixtures/diff_corpus/e13650ac25e75323.der create mode 100644 tests/fixtures/diff_corpus/e339a6843b0f4890.der create mode 100644 tests/fixtures/diff_corpus/e6fe22bf45e4f0d3.der create mode 100644 tests/fixtures/diff_corpus/e793c9b02fd8aa13.der create mode 100644 tests/fixtures/diff_corpus/ea69bc711cb9d456.der create mode 100644 tests/fixtures/diff_corpus/ea6b89ed6907a209.der create mode 100644 tests/fixtures/diff_corpus/ea7a25255d111fc3.der create mode 100644 tests/fixtures/diff_corpus/ec5c5d684fd5c4f5.der create mode 100644 tests/fixtures/diff_corpus/ed13bd4d797ad34f.der create mode 100644 tests/fixtures/diff_corpus/f5165fc624453361.der create mode 100644 tests/fixtures/diff_corpus/fe949f2d842955cc.der create mode 100644 tests/fixtures/diff_corpus/fec41e32ca75c295.der create mode 100644 tests/test_certinfo_corpus.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 936d169..ec76bb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`certinfo.analyze_chain`** (Rust): new PyO3 entry point that parses a full `List[bytes]` DER chain in a single call and returns per-cert details plus adjacent-pair subject/issuer and SKI/AKI linkage — no per-cert PyO3 boundary crossings. - **`SSLHandler.fetch_raw_cert`** now additionally returns `chain_der` and `chain_error`, populated via `SSLSocket.get_verified_chain()` on Python 3.13+ and the stable `_sslobj.get_unverified_chain()` fallback on 3.10–3.12. - **`core._fetch_raw_cert`** parses the chain once on fetch (via `analyze_chain`) and caches the result as `cert_data["chain_analysis"]`, so re-running validators has zero additional cost. +- **In-tree DER / X.509 parser** ([#22](https://github.com/bradh11/certmonitor/issues/22)). `rust_certinfo/src/der/` and `rust_certinfo/src/x509/` are a strict-DER, no-`unsafe`, panic-free, zero-dep replacement for `x509-parser`. The crate is annotated `#![forbid(unsafe_code)]` at the root and every parser path returns `Result<_, ParseError>`. New module structure: `der/{reader,oid,time,string,tag}.rs` for ASN.1 primitives, `x509/{certificate,name,spki,algorithm,extensions}.rs` for the X.509 layer, `pem.rs` and `pyobj.rs` as thin glue, `lib.rs` as the PyO3 entry-point shim. +- **56 in-module Rust unit tests** plus a new corpus snapshot test (`tests/test_certinfo_corpus.py`) that runs every public `certinfo` entry point against 130 unique real-world certs captured from the bench host list. Covers RSA/EC key types, SKI/AKI extraction, validity timestamps, and SPKI extraction for the full corpus. +- **Fuzz harness** at `rust_certinfo/fuzz/` for `Certificate::from_der`. Manual pre-merge gate (nightly + `cargo fuzz`); see `rust_certinfo/fuzz/README.md`. ### Changed -- **Rust dependency footprint shrunk**: the `base64` crate is gone, replaced by an inlined RFC 4648 encoder in `rust_certinfo/src/lib.rs`. `extract_public_key_pem` output is byte-identical. Final Rust deps: `pyo3` + `x509-parser`. +- **Zero non-pyo3 Rust dependencies.** The `x509-parser` crate is gone (replaced by the in-tree parser above) and the `base64` crate is gone (replaced by an inlined RFC 4648 encoder). The Rust dep tree shrinks from **48 crates to 20** — every remaining crate is either `pyo3` itself or a pyo3 build-time helper. `cargo audit` surface drops accordingly. +- **`Cargo.toml`** crate-type now declares `["cdylib", "rlib"]`. The `cdylib` is the same Python wheel target maturin has always built; the additional `rlib` lets the in-repo fuzz crate link against the parser. No published-wheel surface change. +- **Rust public API surface (in-tree only):** `certinfo::Certificate::from_der` and `certinfo::ParseError` are now exposed for use by the fuzz crate and any future in-repo Rust consumer. The PyO3 boundary and Python-facing API are unchanged. ### Fixed -- TBD +- **EC `curve` field now correctly contains the curve OID.** `parse_public_key_info` and the per-cert dict in `analyze_chain` previously emitted the algorithm OID `1.2.840.10045.2.1` (id-ecPublicKey) in the field literally named `curve`. The new parser extracts the curve OID from `algorithm.parameters` and emits e.g. `1.2.840.10045.3.1.7` for P-256, `1.3.132.0.34` for P-384, `1.3.132.0.35` for P-521. Visible behavior change for any caller reading `public_key_info["curve"]`. +- **RSA modulus bit length is no longer over-counted by 8 bits.** The previous build computed bit length as `modulus.len() * 8` from `x509-parser`, which leaves the DER-mandated leading-zero sign byte in `modulus`. Real-world RSA-2048 / 3072 / 4096 keys were reported as 2056 / 3080 / 4104. The new parser strips the sign byte before counting and reports the canonical 2048 / 3072 / 4096. Visible in `public_key_info["size"]` and the chain validator's `public_key_info` per-cert dict. ## [0.2.0] - 2026-04-13 diff --git a/Cargo.lock b/Cargo.lock index d69e6db..1b4350f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,45 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "asn1-rs" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror", - "time", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "autocfg" version = "1.4.0" @@ -52,7 +13,6 @@ name = "certinfo" version = "0.2.0" dependencies = [ "pyo3", - "x509-parser", ] [[package]] @@ -61,46 +21,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "der-parser" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" -dependencies = [ - "asn1-rs", - "displaydoc", - "nom", - "num-bigint", - "num-traits", - "rusticata-macros", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "heck" version = "0.5.0" @@ -113,30 +33,12 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - [[package]] name = "memoffset" version = "0.9.1" @@ -146,65 +48,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "oid-registry" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" -dependencies = [ - "asn1-rs", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -217,12 +60,6 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "proc-macro2" version = "1.0.95" @@ -304,35 +141,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "syn" version = "2.0.101" @@ -344,74 +152,12 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "target-lexicon" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "unicode-ident" version = "1.0.18" @@ -423,20 +169,3 @@ name = "unindent" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - -[[package]] -name = "x509-parser" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom", - "oid-registry", - "rusticata-macros", - "thiserror", - "time", -] diff --git a/Cargo.toml b/Cargo.toml index 4c0d1a7..fe8ea19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,12 @@ edition = "2021" [lib] name = "certinfo" -# This is crucial for building a Python extension -crate-type = ["cdylib"] +# `cdylib` is the Python extension wheel target. `rlib` lets the fuzz +# crate at `rust_certinfo/fuzz/` link the same code as a normal Rust +# library. Maturin still builds the wheel from the `cdylib` artifact; +# the `rlib` adds no published-wheel surface. +crate-type = ["cdylib", "rlib"] path = "rust_certinfo/src/lib.rs" [dependencies] -pyo3 = { version = "0.24.1", features = ["extension-module", "abi3-py38"] } -x509-parser = "0.16.0" \ No newline at end of file +pyo3 = { version = "0.24.1", features = ["extension-module", "abi3-py38"] } \ No newline at end of file diff --git a/MODULARIZATION_REPORT.md b/MODULARIZATION_REPORT.md index 9d1bc17..17c86e1 100644 --- a/MODULARIZATION_REPORT.md +++ b/MODULARIZATION_REPORT.md @@ -10,8 +10,8 @@ ### Test Coverage - **Overall coverage:** 98.8% -- **Total tests:** 407 -- **Statements covered:** 965/977 +- **Total tests:** 425 +- **Statements covered:** 961/973 - **Files with coverage:** 22 ### Type Hint Coverage @@ -30,7 +30,7 @@ - **Python security scanning:** ✅ Enabled - **Python security issues found:** 0 - **Files scanned by bandit:** 22 -- **Lines scanned by bandit:** 2,298 +- **Lines scanned by bandit:** 2,296 - **Overall security status:** 🔒 Clean - **PyO3 version:** 0.24.1 @@ -74,7 +74,7 @@ - **validators/weak_cipher.py**: ✅ (68 lines) - **validators/sensitive_date.py**: ✅ (204 lines) - **validators/subject_alt_names.py**: ✅ (239 lines) -- **validators/chain.py**: ✅ (298 lines) +- **validators/chain.py**: ✅ (302 lines) - **validators/expiration.py**: ✅ (87 lines) - **validators/root_certificate_validator.py**: ✅ (113 lines) - **validators/tls_version.py**: ✅ (70 lines) diff --git a/rust_certinfo/fuzz/Cargo.toml b/rust_certinfo/fuzz/Cargo.toml new file mode 100644 index 0000000..a41cdf1 --- /dev/null +++ b/rust_certinfo/fuzz/Cargo.toml @@ -0,0 +1,29 @@ +# Fuzzing crate for the in-tree certinfo parser. +# +# This is a SEPARATE crate (not part of the main workspace) because +# `cargo fuzz` requires nightly Rust and pulls in `libfuzzer-sys`, which +# we don't want in the published wheel build. +# +# Run: cd rust_certinfo && cargo +nightly fuzz run parse_certificate +# See README.md in this directory for the full pre-merge gate procedure. +[package] +name = "certinfo-fuzz" +version = "0.0.0" +edition = "2021" +publish = false + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.certinfo] +path = "../.." + +[[bin]] +name = "parse_certificate" +path = "fuzz_targets/parse_certificate.rs" +test = false +doc = false +bench = false diff --git a/rust_certinfo/fuzz/README.md b/rust_certinfo/fuzz/README.md new file mode 100644 index 0000000..929546b --- /dev/null +++ b/rust_certinfo/fuzz/README.md @@ -0,0 +1,54 @@ +# certinfo fuzzing + +`cargo fuzz` target for the in-tree X.509 parser. Validates that +`Certificate::from_der` never panics on arbitrary input. This is a +**manual pre-merge gate**, not a CI check — `cargo fuzz` requires the +nightly Rust toolchain and runs for arbitrary durations. + +## When to run + +- Before merging any PR that touches `rust_certinfo/src/der/` or + `rust_certinfo/src/x509/`. +- Before any release tag. + +## Setup (one-time) + +```sh +rustup toolchain install nightly +cargo install cargo-fuzz +``` + +## Run + +```sh +cd rust_certinfo +cargo +nightly fuzz run parse_certificate -- -max_total_time=3600 +``` + +That's a 1-hour run. Use a longer `-max_total_time` (in seconds) for a +deeper soak. The seed corpus is auto-discovered from +`rust_certinfo/fuzz/corpus/parse_certificate/` if present; you can seed +it once with the captured chain corpus: + +```sh +mkdir -p rust_certinfo/fuzz/corpus/parse_certificate +cp tests/fixtures/diff_corpus/*.der rust_certinfo/fuzz/corpus/parse_certificate/ +``` + +## Acceptance gate + +- **Zero crashes** during the run. Any crash is a release blocker. +- The fuzzer's `coverage` and `cov` counters should reach a stable + plateau before the timeout — if they're still climbing rapidly when + the run ends, extend `-max_total_time`. +- Note the `#runs` and `cov` numbers in the PR description so future + reviewers can see the gate was honored. + +## Why this is not in CI + +`cargo fuzz` needs nightly Rust, takes orders of magnitude longer than a +unit test, and pulls in `libfuzzer-sys`. None of those belong in the +PR-time CI matrix. The corpus snapshot test in +`tests/test_certinfo_corpus.py` covers the day-to-day regression check +against real-world certs; this fuzz target is the deeper, slower +defense against malformed input we haven't seen yet. diff --git a/rust_certinfo/fuzz/fuzz_targets/parse_certificate.rs b/rust_certinfo/fuzz/fuzz_targets/parse_certificate.rs new file mode 100644 index 0000000..5f00753 --- /dev/null +++ b/rust_certinfo/fuzz/fuzz_targets/parse_certificate.rs @@ -0,0 +1,20 @@ +// rust_certinfo/fuzz/fuzz_targets/parse_certificate.rs +// +// libFuzzer target. Feeds arbitrary bytes to `Certificate::from_der` and +// asserts the parser never panics. Combined with `#![forbid(unsafe_code)]` +// at the certinfo crate root, this gives us a concrete pre-merge guarantee +// that malformed DER input cannot crash the parser. +// +// Run this target manually as a release-time gate; it is not part of CI. +// See ../README.md for the procedure. + +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // We only care that this returns instead of panicking. Any error + // result is fine — that's what `Certificate::from_der` is supposed + // to do on malformed input. + let _ = certinfo::Certificate::from_der(data); +}); diff --git a/rust_certinfo/src/der.rs b/rust_certinfo/src/der.rs new file mode 100644 index 0000000..b804de5 --- /dev/null +++ b/rust_certinfo/src/der.rs @@ -0,0 +1,14 @@ +// rust_certinfo/src/der.rs +// +// DER primitive layer. This module knows nothing about X.509 — it provides +// the building blocks (TLV reader, OID decoder, time decoder, string +// decoders) that the X.509 layer composes into certificate parsing. + +pub mod oid; +pub mod reader; +pub mod string; +pub mod tag; +pub mod time; + +pub use oid::Oid; +pub use reader::{DerReader, Tlv}; diff --git a/rust_certinfo/src/der/oid.rs b/rust_certinfo/src/der/oid.rs new file mode 100644 index 0000000..a124833 --- /dev/null +++ b/rust_certinfo/src/der/oid.rs @@ -0,0 +1,204 @@ +// rust_certinfo/src/der/oid.rs +// +// OBJECT IDENTIFIER decoder. DER OIDs are a sequence of base-128 arcs: +// each arc's bytes have the high bit set on every byte except the last. +// The first byte combines the first two arcs as `40*X + Y` where X is +// 0, 1, or 2. +// +// We never emit OIDs back to DER — only decode them — so this module is +// decode-only and lives entirely on borrowed slices. + +use crate::error::ParseError; + +/// A borrowed reference to OID body bytes (the value field of an OID TLV, +/// not including the 0x06 tag or the length prefix). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Oid<'a>(&'a [u8]); + +impl<'a> Oid<'a> { + /// Wrap the raw OID body bytes. Validates that the bytes are well-formed. + pub fn from_bytes(bytes: &'a [u8]) -> Result { + if bytes.is_empty() { + return Err(ParseError::InvalidOid); + } + // Validate continuation bit form: every byte except the terminator + // of an arc must have the high bit set; the byte AFTER a terminator + // is the start of the next arc. + let mut i = 0; + while i < bytes.len() { + // Walk one arc. + let arc_start = i; + while i < bytes.len() && bytes[i] & 0x80 != 0 { + i += 1; + } + if i >= bytes.len() { + return Err(ParseError::InvalidOid); + } + // i now points at the terminator byte. + // Reject leading-zero continuation byte (non-canonical). + if i > arc_start && bytes[arc_start] == 0x80 { + return Err(ParseError::InvalidOid); + } + i += 1; + } + Ok(Self(bytes)) + } + + pub fn as_bytes(&self) -> &'a [u8] { + self.0 + } + + /// Render as dotted decimal, e.g. `1.2.840.113549.1.1.11`. + pub fn to_id_string(self) -> String { + let mut out = String::with_capacity(self.0.len() * 2); + let mut bytes = self.0.iter().copied(); + + // First byte combines arc 1 and arc 2. + let first = match bytes.next() { + Some(b) => b, + None => return out, + }; + let arc1 = (first / 40).min(2) as u64; + let arc2 = (first as u64) - 40 * arc1; + out.push_str(&arc1.to_string()); + out.push('.'); + out.push_str(&arc2.to_string()); + + let mut value: u64 = 0; + for b in bytes { + // Multiply existing value by 128 (left shift by 7). + let shifted = match value.checked_shl(7) { + Some(v) => v, + None => { + // Saturate gracefully: this is a parser, not a critical + // crypto path — at worst the OID renders truncated, but + // we already validated the bytes in `from_bytes`. + return out; + } + }; + value = shifted | (b as u64 & 0x7f); + if b & 0x80 == 0 { + out.push('.'); + out.push_str(&value.to_string()); + value = 0; + } + } + out + } +} + +// ---- Well-known OIDs as raw DER body bytes --------------------------------- +// Encoded as the value bytes only (no tag, no length). Generated by hand +// from the dotted-decimal forms; verified by the round-trip tests below. + +/// 1.2.840.113549.1.1.1 — rsaEncryption +pub const OID_RSA_ENCRYPTION: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01]; + +/// 1.2.840.10045.2.1 — id-ecPublicKey +pub const OID_EC_PUBLIC_KEY: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01]; + +// Curve OIDs (RFC 5480, RFC 5639). Used to map curve OID → field bit length. +/// 1.2.840.10045.3.1.7 — secp256r1 / P-256 / prime256v1 +pub const OID_SECP256R1: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]; +/// 1.3.132.0.34 — secp384r1 / P-384 +pub const OID_SECP384R1: &[u8] = &[0x2b, 0x81, 0x04, 0x00, 0x22]; +/// 1.3.132.0.35 — secp521r1 / P-521 +pub const OID_SECP521R1: &[u8] = &[0x2b, 0x81, 0x04, 0x00, 0x23]; +/// 1.3.132.0.10 — secp256k1 (Bitcoin curve, occasionally seen in self-signed certs) +pub const OID_SECP256K1: &[u8] = &[0x2b, 0x81, 0x04, 0x00, 0x0a]; + +// Extension OIDs (RFC 5280) +/// 2.5.29.14 — id-ce-subjectKeyIdentifier +pub const OID_EXT_SKI: &[u8] = &[0x55, 0x1d, 0x0e]; +/// 2.5.29.19 — id-ce-basicConstraints +pub const OID_EXT_BASIC_CONSTRAINTS: &[u8] = &[0x55, 0x1d, 0x13]; +/// 2.5.29.35 — id-ce-authorityKeyIdentifier +pub const OID_EXT_AKI: &[u8] = &[0x55, 0x1d, 0x23]; + +// Attribute Type OIDs for Name parsing (RFC 5280 §4.1.2.4) +/// 2.5.4.3 — id-at-commonName +pub const OID_AT_COMMON_NAME: &[u8] = &[0x55, 0x04, 0x03]; +/// 2.5.4.6 — id-at-countryName +pub const OID_AT_COUNTRY_NAME: &[u8] = &[0x55, 0x04, 0x06]; +/// 2.5.4.10 — id-at-organizationName +pub const OID_AT_ORGANIZATION_NAME: &[u8] = &[0x55, 0x04, 0x0a]; +/// 2.5.4.11 — id-at-organizationalUnitName +pub const OID_AT_ORGANIZATIONAL_UNIT_NAME: &[u8] = &[0x55, 0x04, 0x0b]; + +#[cfg(test)] +mod tests { + use super::*; + + fn roundtrip(bytes: &[u8], expected: &str) { + let oid = Oid::from_bytes(bytes).unwrap(); + assert_eq!(oid.to_id_string(), expected); + } + + #[test] + fn rsa_encryption() { + roundtrip(OID_RSA_ENCRYPTION, "1.2.840.113549.1.1.1"); + } + + #[test] + fn ec_public_key() { + roundtrip(OID_EC_PUBLIC_KEY, "1.2.840.10045.2.1"); + } + + #[test] + fn secp256r1() { + roundtrip(OID_SECP256R1, "1.2.840.10045.3.1.7"); + } + + #[test] + fn secp384r1() { + roundtrip(OID_SECP384R1, "1.3.132.0.34"); + } + + #[test] + fn secp521r1() { + roundtrip(OID_SECP521R1, "1.3.132.0.35"); + } + + #[test] + fn ski() { + roundtrip(OID_EXT_SKI, "2.5.29.14"); + } + + #[test] + fn basic_constraints() { + roundtrip(OID_EXT_BASIC_CONSTRAINTS, "2.5.29.19"); + } + + #[test] + fn aki() { + roundtrip(OID_EXT_AKI, "2.5.29.35"); + } + + #[test] + fn common_name() { + roundtrip(OID_AT_COMMON_NAME, "2.5.4.3"); + } + + #[test] + fn empty_oid_rejected() { + assert_eq!(Oid::from_bytes(&[]).unwrap_err(), ParseError::InvalidOid); + } + + #[test] + fn unterminated_arc_rejected() { + // Last byte has continuation bit set + assert_eq!( + Oid::from_bytes(&[0x2a, 0x86]).unwrap_err(), + ParseError::InvalidOid + ); + } + + #[test] + fn leading_zero_arc_rejected() { + // 0x80 starts an arc with leading zero — non-canonical. + assert_eq!( + Oid::from_bytes(&[0x2a, 0x80, 0x01]).unwrap_err(), + ParseError::InvalidOid + ); + } +} diff --git a/rust_certinfo/src/der/reader.rs b/rust_certinfo/src/der/reader.rs new file mode 100644 index 0000000..87afcf3 --- /dev/null +++ b/rust_certinfo/src/der/reader.rs @@ -0,0 +1,215 @@ +// rust_certinfo/src/der/reader.rs +// +// Strict-DER TLV cursor. The reader walks a `&[u8]` buffer one Tag-Length- +// Value triple at a time, validates DER canonicalization rules on the way +// (no indefinite length, no over-long length encodings, bounds checks +// against the parent slice on every read), and never panics on malformed +// input. All errors flow through `ParseError`. + +use crate::error::ParseError; + +/// One DER Tag-Length-Value element. +#[derive(Debug, Clone, Copy)] +pub struct Tlv<'a> { + pub tag: u8, + /// Bytes of the value field, NOT including the tag and length prefix. + pub value: &'a [u8], + /// Bytes of the entire TLV, including tag and length prefix. Used when + /// the caller needs the raw DER (e.g. `Name::raw`, `SPKI::raw`). + pub raw: &'a [u8], +} + +/// Cursor over a DER byte slice. +#[derive(Debug, Clone, Copy)] +pub struct DerReader<'a> { + input: &'a [u8], + pos: usize, +} + +impl<'a> DerReader<'a> { + pub fn new(input: &'a [u8]) -> Self { + Self { input, pos: 0 } + } + + pub fn is_empty(&self) -> bool { + self.pos >= self.input.len() + } + + /// Assert there are no unread bytes, otherwise return `TrailingBytes`. + pub fn end(self) -> Result<(), ParseError> { + if self.pos == self.input.len() { + Ok(()) + } else { + Err(ParseError::TrailingBytes) + } + } + + /// Peek the next tag byte without advancing. + pub fn peek_tag(&self) -> Option { + self.input.get(self.pos).copied() + } + + /// Read the next TLV. Advances the cursor past the entire element. + pub fn read_tlv(&mut self) -> Result, ParseError> { + let start = self.pos; + let tag = self.read_byte()?; + let length = self.read_length()?; + let value_start = self.pos; + let value_end = value_start + .checked_add(length) + .ok_or(ParseError::IntegerOverflow)?; + if value_end > self.input.len() { + return Err(ParseError::UnexpectedEof); + } + let value = &self.input[value_start..value_end]; + let raw = &self.input[start..value_end]; + self.pos = value_end; + Ok(Tlv { tag, value, raw }) + } + + /// Read the next TLV and require that its tag matches `expected`. + /// Returns the value slice (not including tag/length). + pub fn expect(&mut self, expected: u8) -> Result<&'a [u8], ParseError> { + let tlv = self.read_tlv()?; + if tlv.tag != expected { + return Err(ParseError::UnexpectedTag { + expected, + got: tlv.tag, + }); + } + Ok(tlv.value) + } + + /// Like `expect`, but also returns a sub-reader scoped to the value + /// bytes — convenient for parsing the contents of a SEQUENCE/SET. + pub fn expect_constructed(&mut self, expected: u8) -> Result, ParseError> { + let value = self.expect(expected)?; + Ok(DerReader::new(value)) + } + + /// Read a single byte from the cursor. + fn read_byte(&mut self) -> Result { + let b = *self.input.get(self.pos).ok_or(ParseError::UnexpectedEof)?; + self.pos += 1; + Ok(b) + } + + /// DER length octets (X.690 §8.1.3). + /// + /// Short form: single byte 0x00..=0x7f. + /// Long form: first byte 0x80 | N where N is the number of length + /// bytes that follow; 0x80 alone is indefinite-length and forbidden + /// in DER. The length value itself must use the minimum number of + /// bytes — both `read_length` and the call sites enforce this. + fn read_length(&mut self) -> Result { + let first = self.read_byte()?; + if first < 0x80 { + return Ok(first as usize); + } + if first == 0x80 { + return Err(ParseError::IndefiniteLengthForbidden); + } + let n = (first & 0x7f) as usize; + // Reject pathological length-of-length values. A `usize` on every + // supported platform is at most 8 bytes; we cap at 4 because no + // legitimate certificate has a >4 GiB element. + if n == 0 || n > 4 { + return Err(ParseError::NonCanonicalLength); + } + let mut value: usize = 0; + let length_bytes_start = self.pos; + for _ in 0..n { + let b = self.read_byte()? as usize; + value = value.checked_shl(8).ok_or(ParseError::IntegerOverflow)?; + value = value.checked_add(b).ok_or(ParseError::IntegerOverflow)?; + } + // First length byte must be non-zero (no leading-zero padding) and + // the resulting value must be ≥ 128 (otherwise short form should + // have been used). + if self.input[length_bytes_start] == 0 || value < 0x80 { + return Err(ParseError::NonCanonicalLength); + } + Ok(value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn short_form_length() { + // OCTET STRING with 5-byte content "hello" + let bytes = [0x04, 0x05, b'h', b'e', b'l', b'l', b'o']; + let mut r = DerReader::new(&bytes); + let v = r.expect(0x04).unwrap(); + assert_eq!(v, b"hello"); + assert!(r.end().is_ok()); + } + + #[test] + fn long_form_length() { + // OCTET STRING with 200-byte content + let mut bytes = vec![0x04, 0x81, 200]; + bytes.extend_from_slice(&[0u8; 200]); + let mut r = DerReader::new(&bytes); + let v = r.expect(0x04).unwrap(); + assert_eq!(v.len(), 200); + } + + #[test] + fn indefinite_length_rejected() { + let bytes = [0x30, 0x80, 0x00, 0x00]; + let mut r = DerReader::new(&bytes); + assert_eq!( + r.read_tlv().unwrap_err(), + ParseError::IndefiniteLengthForbidden + ); + } + + #[test] + fn long_form_with_value_under_128_rejected() { + // 0x81 0x42 says "1-byte length, value 0x42" — but 0x42 < 128 so + // short form was required. + let bytes = [0x04, 0x81, 0x42]; + let mut r = DerReader::new(&bytes); + assert_eq!(r.read_tlv().unwrap_err(), ParseError::NonCanonicalLength); + } + + #[test] + fn long_form_with_leading_zero_rejected() { + // 0x82 0x00 0x80 says "2-byte length, value 0x0080" — leading + // zero is forbidden in DER long form. + let bytes = [0x04, 0x82, 0x00, 0x80]; + let mut r = DerReader::new(&bytes); + assert_eq!(r.read_tlv().unwrap_err(), ParseError::NonCanonicalLength); + } + + #[test] + fn truncated_value_rejected() { + let bytes = [0x04, 0x05, b'h', b'e']; + let mut r = DerReader::new(&bytes); + assert_eq!(r.read_tlv().unwrap_err(), ParseError::UnexpectedEof); + } + + #[test] + fn unexpected_tag() { + let bytes = [0x04, 0x01, 0x00]; + let mut r = DerReader::new(&bytes); + assert_eq!( + r.expect(0x02).unwrap_err(), + ParseError::UnexpectedTag { + expected: 0x02, + got: 0x04, + } + ); + } + + #[test] + fn trailing_bytes() { + let bytes = [0x04, 0x01, 0x00, 0xff]; + let mut r = DerReader::new(&bytes); + let _ = r.read_tlv().unwrap(); + assert_eq!(r.end().unwrap_err(), ParseError::TrailingBytes); + } +} diff --git a/rust_certinfo/src/der/string.rs b/rust_certinfo/src/der/string.rs new file mode 100644 index 0000000..c33bc3d --- /dev/null +++ b/rust_certinfo/src/der/string.rs @@ -0,0 +1,163 @@ +// rust_certinfo/src/der/string.rs +// +// Decoders for the ASN.1 string types that show up in X.509 Names. +// +// In practice the public web is dominated by UTF8String and PrintableString. +// IA5String shows up for emailAddress (RFC 1779). TeletexString/T61String, +// BMPString and UniversalString are legacy but still appear in older +// certificates — we accept them rather than crash, but use a conservative +// decode that won't introduce its own parser bugs. + +use crate::der::tag; +use crate::error::ParseError; + +/// Decode a DER string value to an owned `String` based on its ASN.1 tag. +pub fn parse_string(tag_byte: u8, value: &[u8]) -> Result { + match tag_byte { + tag::TAG_UTF8_STRING => parse_utf8(value), + tag::TAG_PRINTABLE_STRING => parse_printable(value), + tag::TAG_IA5_STRING => parse_ascii(value), + tag::TAG_TELETEX_STRING => parse_teletex(value), + tag::TAG_BMP_STRING => parse_bmp(value), + tag::TAG_UNIVERSAL_STRING => parse_universal(value), + other => Err(ParseError::UnsupportedStringType(other)), + } +} + +fn parse_utf8(value: &[u8]) -> Result { + core::str::from_utf8(value) + .map(|s| s.to_string()) + .map_err(|_| ParseError::InvalidString) +} + +/// PrintableString: subset of ASCII (A-Z a-z 0-9 plus a small punctuation set). +/// We do not strictly enforce the alphabet — real-world certs occasionally +/// stretch the rules — but we do require valid 7-bit ASCII. +fn parse_printable(value: &[u8]) -> Result { + parse_ascii(value) +} + +/// IA5String: 7-bit ASCII (the original IA5 alphabet equals US-ASCII for our +/// purposes). +fn parse_ascii(value: &[u8]) -> Result { + if value.iter().any(|&b| b > 0x7f) { + return Err(ParseError::InvalidString); + } + // Safe: validated as ASCII above. + Ok(String::from_utf8_lossy(value).into_owned()) +} + +/// TeletexString / T61String: technically a complex 8-bit encoding, but in +/// practice every cert that uses it stores Latin-1-ish bytes. We pass the +/// bytes through as Latin-1, which is the most common real-world content. +fn parse_teletex(value: &[u8]) -> Result { + Ok(value.iter().map(|&b| b as char).collect()) +} + +/// BMPString: UCS-2 big-endian (the Unicode Basic Multilingual Plane). +/// 2 bytes per code unit. We only accept code points in the BMP. +fn parse_bmp(value: &[u8]) -> Result { + if !value.len().is_multiple_of(2) { + return Err(ParseError::InvalidString); + } + let mut out = String::with_capacity(value.len() / 2); + for chunk in value.chunks_exact(2) { + let code = u16::from_be_bytes([chunk[0], chunk[1]]); + let ch = char::from_u32(code as u32).ok_or(ParseError::InvalidString)?; + out.push(ch); + } + Ok(out) +} + +/// UniversalString: UCS-4 big-endian. 4 bytes per code unit. +fn parse_universal(value: &[u8]) -> Result { + if !value.len().is_multiple_of(4) { + return Err(ParseError::InvalidString); + } + let mut out = String::with_capacity(value.len() / 4); + for chunk in value.chunks_exact(4) { + let code = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + let ch = char::from_u32(code).ok_or(ParseError::InvalidString)?; + out.push(ch); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn utf8_simple() { + assert_eq!( + parse_string(tag::TAG_UTF8_STRING, "example.com".as_bytes()).unwrap(), + "example.com" + ); + } + + #[test] + fn utf8_non_ascii() { + // "café" with é as U+00E9 in UTF-8 + let bytes = [b'c', b'a', b'f', 0xc3, 0xa9]; + assert_eq!(parse_string(tag::TAG_UTF8_STRING, &bytes).unwrap(), "café"); + } + + #[test] + fn utf8_invalid() { + let bad = [b'a', 0xc3, 0x28]; + assert_eq!( + parse_string(tag::TAG_UTF8_STRING, &bad).unwrap_err(), + ParseError::InvalidString + ); + } + + #[test] + fn printable_string() { + assert_eq!( + parse_string(tag::TAG_PRINTABLE_STRING, b"DigiCert Inc").unwrap(), + "DigiCert Inc" + ); + } + + #[test] + fn ia5_string() { + assert_eq!( + parse_string(tag::TAG_IA5_STRING, b"foo@example.com").unwrap(), + "foo@example.com" + ); + } + + #[test] + fn bmp_string_ascii() { + // "AB" as BMPString = 0x00 'A' 0x00 'B' + let bytes = [0x00, b'A', 0x00, b'B']; + assert_eq!(parse_string(tag::TAG_BMP_STRING, &bytes).unwrap(), "AB"); + } + + #[test] + fn bmp_string_odd_length() { + let bytes = [0x00, b'A', 0x00]; + assert_eq!( + parse_string(tag::TAG_BMP_STRING, &bytes).unwrap_err(), + ParseError::InvalidString + ); + } + + #[test] + fn universal_string_ascii() { + // "A" as UniversalString = 0x00 0x00 0x00 'A' + let bytes = [0x00, 0x00, 0x00, b'A']; + assert_eq!( + parse_string(tag::TAG_UNIVERSAL_STRING, &bytes).unwrap(), + "A" + ); + } + + #[test] + fn unsupported_tag() { + assert_eq!( + parse_string(0x42, b"x").unwrap_err(), + ParseError::UnsupportedStringType(0x42) + ); + } +} diff --git a/rust_certinfo/src/der/tag.rs b/rust_certinfo/src/der/tag.rs new file mode 100644 index 0000000..ebb61f5 --- /dev/null +++ b/rust_certinfo/src/der/tag.rs @@ -0,0 +1,33 @@ +// rust_certinfo/src/der/tag.rs +// +// DER tag constants and helpers. We deliberately operate on raw u8 tag +// bytes rather than building a richer Tag struct — the X.509 structures +// we parse only need universal types and a handful of context-specific +// tags, so byte comparison is the simplest correct approach. + +// Universal types (RFC 6025 §8.1 / X.690 §8.1.2.2) +pub const TAG_BOOLEAN: u8 = 0x01; +pub const TAG_INTEGER: u8 = 0x02; +pub const TAG_BIT_STRING: u8 = 0x03; +pub const TAG_OCTET_STRING: u8 = 0x04; +pub const TAG_NULL: u8 = 0x05; +pub const TAG_OBJECT_IDENTIFIER: u8 = 0x06; +pub const TAG_UTF8_STRING: u8 = 0x0c; +pub const TAG_PRINTABLE_STRING: u8 = 0x13; +pub const TAG_TELETEX_STRING: u8 = 0x14; // a.k.a. T61String +pub const TAG_IA5_STRING: u8 = 0x16; +pub const TAG_UTC_TIME: u8 = 0x17; +pub const TAG_GENERALIZED_TIME: u8 = 0x18; +pub const TAG_UNIVERSAL_STRING: u8 = 0x1c; +pub const TAG_BMP_STRING: u8 = 0x1e; + +// Constructed types +pub const TAG_SEQUENCE: u8 = 0x30; // SEQUENCE / SEQUENCE OF +pub const TAG_SET: u8 = 0x31; // SET / SET OF + +// Context-specific tags used by Certificate / TBSCertificate. +// In RFC 5280 these are written as [0], [1], [2], [3] EXPLICIT. +pub const CONTEXT_CONSTRUCTED_0: u8 = 0xa0; // [0] EXPLICIT (version) +pub const CONTEXT_CONSTRUCTED_3: u8 = 0xa3; // [3] EXPLICIT (extensions) + // IMPLICIT [1] / [2] for issuer/subject unique IDs are matched by raw byte + // in the certificate walker; constants for them aren't worth exporting. diff --git a/rust_certinfo/src/der/time.rs b/rust_certinfo/src/der/time.rs new file mode 100644 index 0000000..ae5c1bf --- /dev/null +++ b/rust_certinfo/src/der/time.rs @@ -0,0 +1,208 @@ +// rust_certinfo/src/der/time.rs +// +// UTCTime and GeneralizedTime decoders. Both encode an instant in UTC. +// We return the Unix timestamp in seconds (i64) — same shape as +// `x509-parser`'s `ASN1Time::timestamp()`. +// +// X.509 (RFC 5280 §4.1.2.5) constrains both forms to a single canonical +// shape: UTCTime is YYMMDDHHMMSSZ (13 ASCII bytes), GeneralizedTime is +// YYYYMMDDHHMMSSZ (15 ASCII bytes). Anything else is rejected. + +use crate::der::tag; +use crate::error::ParseError; + +/// Dispatch by ASN.1 tag. Most of the parser knows the tag from context +/// and calls `parse_utc_time` or `parse_generalized_time` directly; this +/// helper exists for `Validity` where the field is a CHOICE. +pub fn parse_time(tag_byte: u8, value: &[u8]) -> Result { + match tag_byte { + tag::TAG_UTC_TIME => parse_utc_time(value), + tag::TAG_GENERALIZED_TIME => parse_generalized_time(value), + other => Err(ParseError::UnexpectedTag { + expected: tag::TAG_UTC_TIME, + got: other, + }), + } +} + +/// UTCTime: YYMMDDHHMMSSZ. Two-digit year is interpreted per RFC 5280 +/// §4.1.2.5.1: `YY >= 50` → 19YY, `YY < 50` → 20YY. +pub fn parse_utc_time(value: &[u8]) -> Result { + if value.len() != 13 || value[12] != b'Z' { + return Err(ParseError::InvalidTime); + } + let year_short = parse_uint(&value[0..2])? as u32; + let year = if year_short < 50 { + 2000 + year_short + } else { + 1900 + year_short + }; + let month = parse_uint(&value[2..4])?; + let day = parse_uint(&value[4..6])?; + let hour = parse_uint(&value[6..8])?; + let minute = parse_uint(&value[8..10])?; + let second = parse_uint(&value[10..12])?; + to_unix_secs(year, month, day, hour, minute, second) +} + +/// GeneralizedTime: YYYYMMDDHHMMSSZ. RFC 5280 forbids fractional seconds +/// in the X.509 profile, so we don't accept them. +pub fn parse_generalized_time(value: &[u8]) -> Result { + if value.len() != 15 || value[14] != b'Z' { + return Err(ParseError::InvalidTime); + } + let year = parse_uint(&value[0..4])? as u32; + let month = parse_uint(&value[4..6])?; + let day = parse_uint(&value[6..8])?; + let hour = parse_uint(&value[8..10])?; + let minute = parse_uint(&value[10..12])?; + let second = parse_uint(&value[12..14])?; + to_unix_secs(year, month, day, hour, minute, second) +} + +/// Parse a fixed-width run of ASCII digits to an integer. +fn parse_uint(bytes: &[u8]) -> Result { + let mut value: u32 = 0; + for &b in bytes { + if !b.is_ascii_digit() { + return Err(ParseError::InvalidTime); + } + value = value + .checked_mul(10) + .and_then(|v| v.checked_add((b - b'0') as u32)) + .ok_or(ParseError::IntegerOverflow)?; + } + Ok(value) +} + +/// Convert (Y, M, D, h, m, s) UTC to Unix seconds. +/// +/// Hand-rolled to avoid pulling in `chrono`/`time` — we already have to +/// own the parser. Algorithm: count days from 1970-01-01 using the +/// civil-from-days approach, then add seconds. +fn to_unix_secs( + year: u32, + month: u32, + day: u32, + hour: u32, + minute: u32, + second: u32, +) -> Result { + if !(1..=12).contains(&month) { + return Err(ParseError::InvalidTime); + } + if day == 0 || day > days_in_month(year, month) { + return Err(ParseError::InvalidTime); + } + if hour > 23 || minute > 59 || second > 60 { + // ASN.1 GeneralizedTime allows leap seconds (60) but X.509 does + // not require us to recognize them as a real instant — we accept + // and clamp to 59 for the unix timestamp math. + return Err(ParseError::InvalidTime); + } + + let days = days_from_civil(year as i32, month as i32, day as i32); + let unix_days = days - 719468; // days from civil 0000-03-01 to 1970-01-01 + let secs = + unix_days * 86_400 + (hour as i64) * 3_600 + (minute as i64) * 60 + second.min(59) as i64; + Ok(secs) +} + +/// Howard Hinnant's days_from_civil. Returns the number of days from the +/// civil epoch 0000-03-01 to the given (proleptic Gregorian) date. +/// Reference: http://howardhinnant.github.io/date_algorithms.html +fn days_from_civil(y: i32, m: i32, d: i32) -> i64 { + let y = if m <= 2 { y - 1 } else { y }; + let era = y.div_euclid(400); + let yoe = (y - era * 400) as i64; // [0, 399] + let doy = ((153 * (m as i64 + if m > 2 { -3 } else { 9 }) + 2) / 5) + d as i64 - 1; + let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096] + (era as i64) * 146_097 + doe +} + +fn is_leap(y: u32) -> bool { + (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400) +} + +fn days_in_month(year: u32, month: u32) -> u32 { + match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 if is_leap(year) => 29, + 2 => 28, + _ => 0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn utc_time_2024_jan_30() { + // "240130000000Z" + let v = b"240130000000Z"; + let ts = parse_utc_time(v).unwrap(); + // 2024-01-30T00:00:00Z = 1706572800 + assert_eq!(ts, 1_706_572_800); + } + + #[test] + fn generalized_time_2030_mar_01() { + // "20300301235959Z" = 2030-03-01T23:59:59Z = 1898639999 + let v = b"20300301235959Z"; + assert_eq!(parse_generalized_time(v).unwrap(), 1_898_639_999); + } + + #[test] + fn utc_time_century_pivot() { + // 49 → 2049, 50 → 1950 + assert_eq!(parse_utc_time(b"491231235959Z").unwrap(), 2_524_607_999); + assert_eq!(parse_utc_time(b"500101000000Z").unwrap(), -631_152_000); + } + + #[test] + fn unix_epoch() { + assert_eq!(parse_utc_time(b"700101000000Z").unwrap(), 0); + } + + #[test] + fn leap_year_feb_29() { + // 2024 is a leap year + let ts = parse_generalized_time(b"20240229000000Z").unwrap(); + assert_eq!(ts, 1_709_164_800); + // 2023 is not — Feb 29 should fail + assert_eq!( + parse_generalized_time(b"20230229000000Z").unwrap_err(), + ParseError::InvalidTime + ); + } + + #[test] + fn rejects_missing_z() { + assert_eq!( + parse_utc_time(b"240130000000X").unwrap_err(), + ParseError::InvalidTime + ); + } + + #[test] + fn rejects_wrong_length() { + assert_eq!( + parse_utc_time(b"24013000Z").unwrap_err(), + ParseError::InvalidTime + ); + assert_eq!( + parse_generalized_time(b"2024013000000Z").unwrap_err(), + ParseError::InvalidTime + ); + } + + #[test] + fn rejects_non_digit() { + assert_eq!( + parse_utc_time(b"24x130000000Z").unwrap_err(), + ParseError::InvalidTime + ); + } +} diff --git a/rust_certinfo/src/error.rs b/rust_certinfo/src/error.rs new file mode 100644 index 0000000..c7eb940 --- /dev/null +++ b/rust_certinfo/src/error.rs @@ -0,0 +1,64 @@ +// rust_certinfo/src/error.rs +// +// All parser errors flow through this single type. Every public function +// in `der/` and `x509/` returns `Result<_, ParseError>` — there are no +// panics on user-supplied bytes. The PyO3 layer in `lib.rs` translates +// `ParseError` to `pyo3::exceptions::PyValueError` exactly as the previous +// `x509_parser` integration did. + +use core::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ParseError { + /// Reader hit end-of-input before a structural element finished. + UnexpectedEof, + /// A constructed/sequence had bytes left over after the expected fields. + TrailingBytes, + /// Tag at this position did not match what the structure required. + UnexpectedTag { expected: u8, got: u8 }, + /// Indefinite-length encoding is BER-only and forbidden in DER. + IndefiniteLengthForbidden, + /// Length used long form when short form would have sufficed, or + /// long form used more bytes than necessary. + NonCanonicalLength, + /// OID bytes did not decode to a valid dotted identifier. + InvalidOid, + /// Bytes claimed to be a UTF-8 / ASCII string failed validation. + InvalidString, + /// UTCTime / GeneralizedTime did not parse to a real instant. + InvalidTime, + /// String tag was outside the set we know how to decode. + UnsupportedStringType(u8), + /// A length or count overflowed our usable range. + IntegerOverflow, + /// BIT STRING had a non-zero "unused bits" prefix where one is not allowed. + InvalidBitString, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnexpectedEof => f.write_str("unexpected end of input"), + Self::TrailingBytes => f.write_str("trailing bytes after expected structure"), + Self::UnexpectedTag { expected, got } => { + write!( + f, + "unexpected tag: expected 0x{:02x}, got 0x{:02x}", + expected, got + ) + } + Self::IndefiniteLengthForbidden => { + f.write_str("indefinite-length encoding is forbidden in DER") + } + Self::NonCanonicalLength => f.write_str("non-canonical DER length encoding"), + Self::InvalidOid => f.write_str("invalid OBJECT IDENTIFIER encoding"), + Self::InvalidString => f.write_str("invalid string encoding"), + Self::InvalidTime => f.write_str("invalid time encoding"), + Self::UnsupportedStringType(t) => write!(f, "unsupported string tag 0x{:02x}", t), + Self::IntegerOverflow => f.write_str("integer overflow"), + Self::InvalidBitString => f.write_str("invalid BIT STRING encoding"), + } + } +} + +impl std::error::Error for ParseError {} diff --git a/rust_certinfo/src/lib.rs b/rust_certinfo/src/lib.rs index 156bcfc..756d32c 100644 --- a/rust_certinfo/src/lib.rs +++ b/rust_certinfo/src/lib.rs @@ -1,390 +1,71 @@ -// src/lib.rs +// rust_certinfo/src/lib.rs +// +// PyO3 module for the `certinfo` Python extension. This file is a thin +// shim — the actual parsing lives in `crate::der` (DER primitives) and +// `crate::x509` (RFC 5280 structures), with the Python-facing dict +// conversions in `crate::pyobj`. +// +// Hard guarantees enforced at the crate level: +// - No `unsafe` anywhere in our code (`forbid(unsafe_code)`). +// - No panics on malformed input (every parser path returns `Result`). +// - Zero non-pyo3 runtime dependencies. + +#![forbid(unsafe_code)] -use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::{PyBytes, PyDict, PyList}; -use x509_parser::prelude::*; -use x509_parser::public_key::PublicKey; +use pyo3::types::{PyBytes, PyDict}; -// Minimal RFC 4648 base64 encoder (standard alphabet, with padding). -// Encode-only; we only need it to wrap SPKI DER into PEM. Kept inline so -// the crate has no runtime dependency on the `base64` crate. -const B64_ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +mod der; +mod error; +mod pem; +mod pyobj; +mod x509; -fn b64_encode(input: &[u8]) -> String { - let mut out = String::with_capacity(input.len().div_ceil(3) * 4); - let mut chunks = input.chunks_exact(3); - for chunk in chunks.by_ref() { - let n = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | (chunk[2] as u32); - out.push(B64_ALPHABET[((n >> 18) & 0x3F) as usize] as char); - out.push(B64_ALPHABET[((n >> 12) & 0x3F) as usize] as char); - out.push(B64_ALPHABET[((n >> 6) & 0x3F) as usize] as char); - out.push(B64_ALPHABET[(n & 0x3F) as usize] as char); - } - let rem = chunks.remainder(); - match rem.len() { - 0 => {} - 1 => { - let n = (rem[0] as u32) << 16; - out.push(B64_ALPHABET[((n >> 18) & 0x3F) as usize] as char); - out.push(B64_ALPHABET[((n >> 12) & 0x3F) as usize] as char); - out.push('='); - out.push('='); - } - 2 => { - let n = ((rem[0] as u32) << 16) | ((rem[1] as u32) << 8); - out.push(B64_ALPHABET[((n >> 18) & 0x3F) as usize] as char); - out.push(B64_ALPHABET[((n >> 12) & 0x3F) as usize] as char); - out.push(B64_ALPHABET[((n >> 6) & 0x3F) as usize] as char); - out.push('='); - } - _ => unreachable!(), - } - out -} - -/// A small struct to hold the parsed key info in Rust -#[derive(Debug, Clone)] -struct KeyInfo { - algorithm: String, - size: usize, - curve: Option, -} - -impl KeyInfo { - fn new(algorithm: &str, size: usize, curve: Option) -> Self { - KeyInfo { - algorithm: algorithm.to_string(), - size, - curve, - } - } -} - -fn extract_key_info(cert: &X509Certificate) -> KeyInfo { - let spki = cert.public_key(); - match spki.parsed() { - Ok(PublicKey::RSA(rsa)) => { - let bits = rsa.modulus.len() * 8; - KeyInfo::new("rsaEncryption", bits, None) - } - Ok(PublicKey::EC(ec_point)) => { - let bits = ec_point.key_size(); - let curve_oid = spki.algorithm.oid().to_id_string(); - KeyInfo::new("ecPublicKey", bits, Some(curve_oid)) - } - Ok(_) => KeyInfo::new("unknown", 0, None), - Err(_) => KeyInfo::new("unknown", 0, None), - } -} +// Public Rust API. The Python wheel doesn't use these — the wheel calls +// the `#[pyfunction]` entry points further down — but the in-repo fuzz +// crate at `rust_certinfo/fuzz/` does, and any future in-tree Rust +// consumer (e.g. a CLI) can use the same surface. +pub use crate::error::ParseError; +pub use crate::x509::Certificate; -fn key_info_to_pydict<'py>(py: Python<'py>, key_info: &KeyInfo) -> Bound<'py, PyDict> { - let dict = PyDict::new(py); - dict.set_item("algorithm", &key_info.algorithm).unwrap(); - dict.set_item("size", key_info.size).unwrap(); - match &key_info.curve { - Some(curve) => dict.set_item("curve", curve).unwrap(), - None => dict.set_item("curve", py.None()).unwrap(), - } - dict -} +use crate::pyobj::to_py_err; -/// Parse the DER bytes of an X.509 certificate and extract public key info. +/// Parse an X.509 certificate (DER) and return public key info as a dict +/// `{"algorithm": str, "size": int, "curve": str | None}`. /// -/// Returns a Python dictionary with: -/// - "algorithm": "rsaEncryption" or "ecPublicKey" or "unknown" -/// - "size": the bit length (e.g., 2048 for RSA) -/// - "curve": the curve OID string if EC, or None for RSA +/// For EC keys the `curve` field contains the curve OID (e.g. +/// `"1.2.840.10045.3.1.7"` for P-256). Earlier builds incorrectly returned +/// the algorithm OID here. #[pyfunction] -fn parse_public_key_info(der_data: Vec) -> PyResult> { - let (_, certificate) = X509Certificate::from_der(&der_data) - .map_err(|_| PyValueError::new_err("Failed to parse X.509 certificate"))?; - let key_info = extract_key_info(&certificate); - - let py_dict = Python::with_gil(|py| { - let dict = key_info_to_pydict(py, &key_info); - dict.into() - }); - - Ok(py_dict) +fn parse_public_key_info(py: Python<'_>, der_data: Vec) -> PyResult> { + let cert = Certificate::from_der(&der_data).map_err(to_py_err)?; + let dict = pyobj::key_info_dict(py, &cert.spki)?; + Ok(dict.into()) } -/// Extract the public key from a certificate in DER format. -/// -/// Returns the DER-encoded SubjectPublicKeyInfo as bytes. +/// Extract the SubjectPublicKeyInfo as raw DER bytes. #[pyfunction] -fn extract_public_key_der(der_data: Vec) -> PyResult> { - let (_, certificate) = X509Certificate::from_der(&der_data) - .map_err(|_| PyValueError::new_err("Failed to parse X.509 certificate"))?; - - let spki_der = certificate.public_key().raw; - - Python::with_gil(|py| { - let py_bytes = PyBytes::new(py, spki_der); - Ok(py_bytes.into()) - }) +fn extract_public_key_der(py: Python<'_>, der_data: Vec) -> PyResult> { + let cert = Certificate::from_der(&der_data).map_err(to_py_err)?; + let bytes = PyBytes::new(py, cert.spki.raw); + Ok(bytes.into()) } -/// Extract the public key from a certificate in PEM format. -/// -/// Returns the PEM-encoded SubjectPublicKeyInfo as a string. +/// Extract the SubjectPublicKeyInfo as a PEM-encoded string. #[pyfunction] fn extract_public_key_pem(der_data: Vec) -> PyResult { - let (_, certificate) = X509Certificate::from_der(&der_data) - .map_err(|_| PyValueError::new_err("Failed to parse X.509 certificate"))?; - - let spki_der = certificate.public_key().raw; - let encoded = b64_encode(spki_der); - - let wrapped = encoded - .as_bytes() - .chunks(64) - .map(|c| std::str::from_utf8(c).unwrap()) - .collect::>() - .join("\n"); - - Ok(format!( - "-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----", - wrapped - )) -} - -fn collect_name_fields(name: &X509Name) -> Vec<(&'static str, String)> { - let mut fields: Vec<(&'static str, String)> = Vec::new(); - if let Some(s) = name.iter_common_name().next().and_then(|a| a.as_str().ok()) { - fields.push(("commonName", s.to_string())); - } - if let Some(s) = name - .iter_organization() - .next() - .and_then(|a| a.as_str().ok()) - { - fields.push(("organizationName", s.to_string())); - } - if let Some(s) = name - .iter_organizational_unit() - .next() - .and_then(|a| a.as_str().ok()) - { - fields.push(("organizationalUnitName", s.to_string())); - } - if let Some(s) = name.iter_country().next().and_then(|a| a.as_str().ok()) { - fields.push(("countryName", s.to_string())); - } - fields -} - -fn hex_string(bytes: &[u8]) -> String { - let mut out = String::with_capacity(bytes.len() * 2); - for b in bytes { - out.push_str(&format!("{:02x}", b)); - } - out -} - -fn is_weak_signature(oid: &str) -> bool { - matches!( - oid, - "1.2.840.113549.1.1.5" // sha1WithRSAEncryption - | "1.2.840.113549.1.1.4" // md5WithRSAEncryption - | "1.2.840.113549.1.1.2" // md2WithRSAEncryption - | "1.2.840.10045.4.1" // ecdsa-with-SHA1 - | "1.2.840.10040.4.3" // dsa-with-sha1 - ) -} - -struct CertData { - subject_raw: Vec, - issuer_raw: Vec, - subject_fields: Vec<(&'static str, String)>, - issuer_fields: Vec<(&'static str, String)>, - not_before_unix: i64, - not_after_unix: i64, - serial_hex: String, - sig_oid: String, - is_ca: bool, - ski: Option, - aki: Option, - key_info: KeyInfo, -} - -fn extract_cert_data(cert: &X509Certificate) -> CertData { - let mut ski: Option = None; - let mut aki: Option = None; - let mut is_ca = false; - - for ext in cert.extensions() { - match ext.parsed_extension() { - ParsedExtension::SubjectKeyIdentifier(k) => { - ski = Some(hex_string(k.0)); - } - ParsedExtension::AuthorityKeyIdentifier(a) => { - aki = a.key_identifier.as_ref().map(|k| hex_string(k.0)); - } - ParsedExtension::BasicConstraints(bc) => { - is_ca = bc.ca; - } - _ => {} - } - } - - CertData { - subject_raw: cert.subject().as_raw().to_vec(), - issuer_raw: cert.issuer().as_raw().to_vec(), - subject_fields: collect_name_fields(cert.subject()), - issuer_fields: collect_name_fields(cert.issuer()), - not_before_unix: cert.validity().not_before.timestamp(), - not_after_unix: cert.validity().not_after.timestamp(), - serial_hex: hex_string(cert.raw_serial()), - sig_oid: cert.signature_algorithm.algorithm.to_id_string(), - is_ca, - ski, - aki, - key_info: extract_key_info(cert), - } + let cert = Certificate::from_der(&der_data).map_err(to_py_err)?; + Ok(pem::wrap_spki_pem(cert.spki.raw)) } -/// Parse a full TLS certificate chain and report per-cert details plus -/// adjacent-pair linkage. Structural validation only — no cryptographic -/// signature verification (which would require pulling in a crypto crate). -/// -/// Input: `chain_ders` is an ordered list of DER-encoded X.509 certificates, -/// leaf first. Typically from `SSLSocket.get_verified_chain()` on Python 3.13+ -/// or `SSLSocket._sslobj.get_unverified_chain()` on 3.10–3.12. -/// -/// Returns a Python dictionary shaped like: -/// { -/// "chain_length": int, -/// "certs": [ { per-cert details... }, ... ], -/// "links": [ { subject_matches_issuer, aki_matches_ski }, ... ], -/// "ordered": bool, -/// "terminates_in_self_signed": bool, -/// } +/// Parse an entire TLS certificate chain in one call. See +/// `crate::pyobj::analyze_chain_dict` for the result shape. #[pyfunction] -fn analyze_chain(chain_ders: Vec>) -> PyResult> { - let mut data: Vec = Vec::with_capacity(chain_ders.len()); - for (i, der) in chain_ders.iter().enumerate() { - let (_, cert) = X509Certificate::from_der(der).map_err(|_| { - PyValueError::new_err(format!( - "Failed to parse certificate at chain position {}", - i - )) - })?; - data.push(extract_cert_data(&cert)); - } - - Python::with_gil(|py| { - let top = PyDict::new(py); - let certs_list = PyList::empty(py); - let mut terminates_in_self_signed = false; - - for (i, cd) in data.iter().enumerate() { - let cert_dict = PyDict::new(py); - cert_dict.set_item("position", i).unwrap(); - - let subject_dict = PyDict::new(py); - for (k, v) in &cd.subject_fields { - subject_dict.set_item(*k, v).unwrap(); - } - cert_dict.set_item("subject", subject_dict).unwrap(); - - let issuer_dict = PyDict::new(py); - for (k, v) in &cd.issuer_fields { - issuer_dict.set_item(*k, v).unwrap(); - } - cert_dict.set_item("issuer", issuer_dict).unwrap(); - - cert_dict - .set_item("not_before_unix", cd.not_before_unix) - .unwrap(); - cert_dict - .set_item("not_after_unix", cd.not_after_unix) - .unwrap(); - cert_dict.set_item("serial_number", &cd.serial_hex).unwrap(); - cert_dict - .set_item("signature_algorithm_oid", &cd.sig_oid) - .unwrap(); - cert_dict - .set_item("signature_algorithm_weak", is_weak_signature(&cd.sig_oid)) - .unwrap(); - cert_dict.set_item("is_ca", cd.is_ca).unwrap(); - - match &cd.ski { - Some(v) => cert_dict.set_item("subject_key_identifier", v).unwrap(), - None => cert_dict - .set_item("subject_key_identifier", py.None()) - .unwrap(), - } - match &cd.aki { - Some(v) => cert_dict.set_item("authority_key_identifier", v).unwrap(), - None => cert_dict - .set_item("authority_key_identifier", py.None()) - .unwrap(), - } - - let dn_self_match = cd.subject_raw == cd.issuer_raw; - let ski_aki_ok = match (&cd.ski, &cd.aki) { - (Some(ski), Some(aki)) => ski == aki, - _ => true, - }; - let is_self_signed = dn_self_match && ski_aki_ok; - cert_dict - .set_item("is_self_signed", is_self_signed) - .unwrap(); - - cert_dict - .set_item("public_key_info", key_info_to_pydict(py, &cd.key_info)) - .unwrap(); - - if i == data.len() - 1 && is_self_signed { - terminates_in_self_signed = true; - } - - certs_list.append(cert_dict).unwrap(); - } - - let links_list = PyList::empty(py); - let mut ordered = true; - - for i in 0..data.len().saturating_sub(1) { - let child = &data[i]; - let parent = &data[i + 1]; - - let subject_matches_issuer = child.issuer_raw == parent.subject_raw; - let aki_matches_ski: Option = match (&child.aki, &parent.ski) { - (Some(a), Some(s)) => Some(a == s), - _ => None, - }; - - let link = PyDict::new(py); - link.set_item("subject_matches_issuer", subject_matches_issuer) - .unwrap(); - match aki_matches_ski { - Some(v) => link.set_item("aki_matches_ski", v).unwrap(), - None => link.set_item("aki_matches_ski", py.None()).unwrap(), - } - links_list.append(link).unwrap(); - - if !subject_matches_issuer { - ordered = false; - } - if let Some(false) = aki_matches_ski { - ordered = false; - } - } - - top.set_item("chain_length", data.len()).unwrap(); - top.set_item("certs", certs_list).unwrap(); - top.set_item("links", links_list).unwrap(); - top.set_item("ordered", ordered).unwrap(); - top.set_item("terminates_in_self_signed", terminates_in_self_signed) - .unwrap(); - - Ok(top.into()) - }) +fn analyze_chain(py: Python<'_>, chain_ders: Vec>) -> PyResult> { + let dict: Bound<'_, PyDict> = pyobj::analyze_chain_dict(py, &chain_ders)?; + Ok(dict.into()) } -/// The module definition. This tells PyO3 to create a Python module named `certinfo`. #[pymodule] fn certinfo(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(parse_public_key_info, m)?)?; diff --git a/rust_certinfo/src/pem.rs b/rust_certinfo/src/pem.rs new file mode 100644 index 0000000..a0dc561 --- /dev/null +++ b/rust_certinfo/src/pem.rs @@ -0,0 +1,110 @@ +// rust_certinfo/src/pem.rs +// +// Minimal PEM helper. The previous implementation pulled in the `base64` +// crate; this replacement uses an inlined RFC 4648 encoder so the crate +// has zero non-pyo3 runtime deps. + +const B64_ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +pub fn b64_encode(input: &[u8]) -> String { + let mut out = String::with_capacity(input.len().div_ceil(3) * 4); + let mut chunks = input.chunks_exact(3); + for chunk in chunks.by_ref() { + let n = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | (chunk[2] as u32); + out.push(B64_ALPHABET[((n >> 18) & 0x3F) as usize] as char); + out.push(B64_ALPHABET[((n >> 12) & 0x3F) as usize] as char); + out.push(B64_ALPHABET[((n >> 6) & 0x3F) as usize] as char); + out.push(B64_ALPHABET[(n & 0x3F) as usize] as char); + } + let rem = chunks.remainder(); + match rem.len() { + 0 => {} + 1 => { + let n = (rem[0] as u32) << 16; + out.push(B64_ALPHABET[((n >> 18) & 0x3F) as usize] as char); + out.push(B64_ALPHABET[((n >> 12) & 0x3F) as usize] as char); + out.push('='); + out.push('='); + } + 2 => { + let n = ((rem[0] as u32) << 16) | ((rem[1] as u32) << 8); + out.push(B64_ALPHABET[((n >> 18) & 0x3F) as usize] as char); + out.push(B64_ALPHABET[((n >> 12) & 0x3F) as usize] as char); + out.push(B64_ALPHABET[((n >> 6) & 0x3F) as usize] as char); + out.push('='); + } + _ => unreachable!(), + } + out +} + +/// Wrap raw SubjectPublicKeyInfo DER bytes in a PEM block. Output is +/// byte-identical to the previous `extract_public_key_pem` so the +/// differential test passes for non-EC certs and for the SPKI path. +pub fn wrap_spki_pem(spki_der: &[u8]) -> String { + let encoded = b64_encode(spki_der); + let wrapped = encoded + .as_bytes() + .chunks(64) + .map(|c| std::str::from_utf8(c).expect("base64 alphabet is ASCII")) + .collect::>() + .join("\n"); + format!( + "-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----", + wrapped + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn b64_empty() { + assert_eq!(b64_encode(b""), ""); + } + + #[test] + fn b64_one_byte() { + // RFC 4648 §10: "f" → "Zg==" + assert_eq!(b64_encode(b"f"), "Zg=="); + } + + #[test] + fn b64_two_bytes() { + assert_eq!(b64_encode(b"fo"), "Zm8="); + } + + #[test] + fn b64_three_bytes() { + assert_eq!(b64_encode(b"foo"), "Zm9v"); + } + + #[test] + fn b64_long() { + // "Hello world" → "SGVsbG8gd29ybGQ=" + assert_eq!(b64_encode(b"Hello world"), "SGVsbG8gd29ybGQ="); + } + + #[test] + fn pem_wrapping_at_64() { + // 48 bytes of input → 64 chars of base64 (one full line, no wrap). + let input = vec![0u8; 48]; + let pem = wrap_spki_pem(&input); + let lines: Vec<&str> = pem.lines().collect(); + assert_eq!(lines[0], "-----BEGIN PUBLIC KEY-----"); + assert_eq!(lines[lines.len() - 1], "-----END PUBLIC KEY-----"); + // Body is exactly one 64-char line + assert_eq!(lines[1].len(), 64); + assert_eq!(lines.len(), 3); + } + + #[test] + fn pem_wrapping_over_64() { + let input = vec![0u8; 96]; + let pem = wrap_spki_pem(&input); + let lines: Vec<&str> = pem.lines().collect(); + assert!(lines[1].len() <= 64); + assert!(lines[2].len() <= 64); + } +} diff --git a/rust_certinfo/src/pyobj.rs b/rust_certinfo/src/pyobj.rs new file mode 100644 index 0000000..e3c5190 --- /dev/null +++ b/rust_certinfo/src/pyobj.rs @@ -0,0 +1,229 @@ +// rust_certinfo/src/pyobj.rs +// +// Bridge between the pure-Rust X.509 layer and the PyO3 entry points in +// `lib.rs`. This is the only file in the crate that knows about Python. +// Keeping it isolated means the parser can be reasoned about (and unit +// tested) entirely in Rust, without GIL acquisition. + +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyList}; + +use crate::error::ParseError; +use crate::x509::{Certificate, Name, PublicKeyAlgorithm, SubjectPublicKeyInfo}; + +/// Map a `ParseError` to a `PyValueError`. Single seam for error +/// translation; lets the rest of the crate stay PyO3-free. +pub fn to_py_err(err: ParseError) -> PyErr { + pyo3::exceptions::PyValueError::new_err(format!("X.509 parse error: {}", err)) +} + +/// Build the `{"algorithm": ..., "size": ..., "curve": ...}` dict that +/// `parse_public_key_info` returns. Mirrors the previous shape exactly, +/// **except** the `curve` field for EC keys now correctly contains the +/// curve OID (e.g. `1.2.840.10045.3.1.7`) instead of the algorithm OID. +pub fn key_info_dict<'py>( + py: Python<'py>, + spki: &SubjectPublicKeyInfo<'_>, +) -> PyResult> { + let dict = PyDict::new(py); + match spki.parsed() { + PublicKeyAlgorithm::Rsa { modulus_bits } => { + dict.set_item("algorithm", "rsaEncryption")?; + dict.set_item("size", modulus_bits)?; + dict.set_item("curve", py.None())?; + } + PublicKeyAlgorithm::Ec { + curve_oid, + key_bits, + } => { + dict.set_item("algorithm", "ecPublicKey")?; + dict.set_item("size", key_bits)?; + dict.set_item("curve", curve_oid.to_id_string())?; + } + PublicKeyAlgorithm::Unknown => { + dict.set_item("algorithm", "unknown")?; + dict.set_item("size", 0usize)?; + dict.set_item("curve", py.None())?; + } + } + Ok(dict) +} + +/// Build the per-cert dict used by `analyze_chain`. Mirrors the previous +/// shape exactly so the chain validator and its tests do not need to +/// change. +fn cert_dict<'py>( + py: Python<'py>, + position: usize, + cert: &Certificate<'_>, +) -> PyResult> { + let d = PyDict::new(py); + d.set_item("position", position)?; + d.set_item("subject", name_dict(py, &cert.subject)?)?; + d.set_item("issuer", name_dict(py, &cert.issuer)?)?; + d.set_item("not_before_unix", cert.validity.not_before_unix)?; + d.set_item("not_after_unix", cert.validity.not_after_unix)?; + d.set_item("serial_number", hex_string(cert.serial_raw))?; + + let sig_oid = cert.signature_algorithm.algorithm.to_id_string(); + d.set_item("signature_algorithm_weak", is_weak_signature(&sig_oid))?; + d.set_item("signature_algorithm_oid", sig_oid)?; + + let bc = cert.extensions.basic_constraints().map_err(to_py_err)?; + d.set_item("is_ca", bc.map(|c| c.ca).unwrap_or(false))?; + + let ski = cert + .extensions + .subject_key_identifier() + .map_err(to_py_err)? + .map(hex_string); + let aki = cert + .extensions + .authority_key_identifier() + .map_err(to_py_err)? + .and_then(|aki| aki.key_identifier.map(hex_string)); + + match &ski { + Some(s) => d.set_item("subject_key_identifier", s)?, + None => d.set_item("subject_key_identifier", py.None())?, + }; + match &aki { + Some(s) => d.set_item("authority_key_identifier", s)?, + None => d.set_item("authority_key_identifier", py.None())?, + }; + + // Self-signed = subject == issuer (raw DN equality) AND, when both + // SKI and AKI are present, they match. Mirrors the previous logic. + let dn_self_match = cert.subject.raw == cert.issuer.raw; + let ski_aki_ok = match (&ski, &aki) { + (Some(ski), Some(aki)) => ski == aki, + _ => true, + }; + d.set_item("is_self_signed", dn_self_match && ski_aki_ok)?; + + d.set_item("public_key_info", key_info_dict(py, &cert.spki)?)?; + + Ok(d) +} + +fn name_dict<'py>(py: Python<'py>, name: &Name<'_>) -> PyResult> { + let d = PyDict::new(py); + if let Some(cn) = name.common_name() { + d.set_item("commonName", cn)?; + } + if let Some(o) = name.organization() { + d.set_item("organizationName", o)?; + } + if let Some(ou) = name.organizational_unit() { + d.set_item("organizationalUnitName", ou)?; + } + if let Some(c) = name.country() { + d.set_item("countryName", c)?; + } + Ok(d) +} + +/// Lowercase hex with no separators. Used for serial number, SKI, AKI. +pub fn hex_string(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len() * 2); + for b in bytes { + out.push_str(&format!("{:02x}", b)); + } + out +} + +pub fn is_weak_signature(oid: &str) -> bool { + matches!( + oid, + "1.2.840.113549.1.1.5" // sha1WithRSAEncryption + | "1.2.840.113549.1.1.4" // md5WithRSAEncryption + | "1.2.840.113549.1.1.2" // md2WithRSAEncryption + | "1.2.840.10045.4.1" // ecdsa-with-SHA1 + | "1.2.840.10040.4.3" // dsa-with-sha1 + ) +} + +/// Build the top-level `analyze_chain` result dict. Mirrors the previous +/// shape so the Python chain validator tests pass unchanged. +pub fn analyze_chain_dict<'py>( + py: Python<'py>, + chain_ders: &[Vec], +) -> PyResult> { + // Parse all certs up front so we can compute linkage between adjacent + // pairs without re-parsing. + let mut parsed: Vec> = Vec::with_capacity(chain_ders.len()); + for (i, der) in chain_ders.iter().enumerate() { + let cert = Certificate::from_der(der).map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!( + "Failed to parse certificate at chain position {}: {}", + i, e + )) + })?; + parsed.push(cert); + } + + let top = PyDict::new(py); + let certs_list = PyList::empty(py); + let mut terminates_in_self_signed = false; + + // Pre-compute per-cert SKI/AKI hex once so the link loop can reuse + // them without re-walking extensions. + let mut per_cert_ski: Vec> = Vec::with_capacity(parsed.len()); + let mut per_cert_aki: Vec> = Vec::with_capacity(parsed.len()); + + for (i, cert) in parsed.iter().enumerate() { + let dict = cert_dict(py, i, cert)?; + let ski = match dict.get_item("subject_key_identifier")? { + Some(v) if !v.is_none() => Some(v.extract::()?), + _ => None, + }; + let aki = match dict.get_item("authority_key_identifier")? { + Some(v) if !v.is_none() => Some(v.extract::()?), + _ => None, + }; + per_cert_ski.push(ski); + per_cert_aki.push(aki); + + if i == parsed.len() - 1 { + if let Some(v) = dict.get_item("is_self_signed")? { + terminates_in_self_signed = v.extract::()?; + } + } + certs_list.append(dict)?; + } + + let links_list = PyList::empty(py); + let mut ordered = true; + + for i in 0..parsed.len().saturating_sub(1) { + let child = &parsed[i]; + let parent = &parsed[i + 1]; + let subject_matches_issuer = child.issuer.raw == parent.subject.raw; + let aki_matches_ski: Option = match (&per_cert_aki[i], &per_cert_ski[i + 1]) { + (Some(a), Some(s)) => Some(a == s), + _ => None, + }; + + let link = PyDict::new(py); + link.set_item("subject_matches_issuer", subject_matches_issuer)?; + match aki_matches_ski { + Some(v) => link.set_item("aki_matches_ski", v)?, + None => link.set_item("aki_matches_ski", py.None())?, + }; + links_list.append(link)?; + + if !subject_matches_issuer { + ordered = false; + } + if let Some(false) = aki_matches_ski { + ordered = false; + } + } + + top.set_item("chain_length", parsed.len())?; + top.set_item("certs", certs_list)?; + top.set_item("links", links_list)?; + top.set_item("ordered", ordered)?; + top.set_item("terminates_in_self_signed", terminates_in_self_signed)?; + Ok(top) +} diff --git a/rust_certinfo/src/x509.rs b/rust_certinfo/src/x509.rs new file mode 100644 index 0000000..ae0547b --- /dev/null +++ b/rust_certinfo/src/x509.rs @@ -0,0 +1,17 @@ +// rust_certinfo/src/x509.rs +// +// X.509 layer. Composes the DER primitives in `crate::der` into the +// certificate structures defined in RFC 5280. Knows nothing about PyO3 +// — that translation lives in `crate::pyobj`. + +pub mod algorithm; +pub mod certificate; +pub mod extensions; +pub mod name; +pub mod spki; + +// Re-exports for the `lib.rs` shim and `pyobj.rs` converters. Other types +// are reachable via their parent module path. +pub use certificate::Certificate; +pub use name::Name; +pub use spki::{PublicKeyAlgorithm, SubjectPublicKeyInfo}; diff --git a/rust_certinfo/src/x509/algorithm.rs b/rust_certinfo/src/x509/algorithm.rs new file mode 100644 index 0000000..1b8da8a --- /dev/null +++ b/rust_certinfo/src/x509/algorithm.rs @@ -0,0 +1,101 @@ +// rust_certinfo/src/x509/algorithm.rs +// +// AlgorithmIdentifier ::= SEQUENCE { +// algorithm OBJECT IDENTIFIER, +// parameters ANY DEFINED BY algorithm OPTIONAL +// } +// +// We expose `parameters` as the raw TLV slice (tag + length + value) so the +// caller can re-parse it for whatever type the algorithm uses. For EC keys +// the parameters are an ECParameters CHOICE which in practice is always a +// named-curve OID — `x509::spki` re-parses that OID itself. + +use crate::der::{tag, DerReader, Oid, Tlv}; +use crate::error::ParseError; + +#[derive(Debug, Clone, Copy)] +pub struct AlgorithmIdentifier<'a> { + pub algorithm: Oid<'a>, + /// Raw parameters TLV (tag + length + value), or None if absent / NULL. + pub parameters: Option<&'a [u8]>, +} + +impl<'a> AlgorithmIdentifier<'a> { + /// Parse an AlgorithmIdentifier from a sub-reader positioned at its + /// outer SEQUENCE tag. + pub fn parse(reader: &mut DerReader<'a>) -> Result { + let mut inner = reader.expect_constructed(tag::TAG_SEQUENCE)?; + Self::parse_inner(&mut inner) + } + + /// Parse contents of an already-unwrapped AlgorithmIdentifier SEQUENCE. + pub fn parse_inner(inner: &mut DerReader<'a>) -> Result { + let oid_value = inner.expect(tag::TAG_OBJECT_IDENTIFIER)?; + let algorithm = Oid::from_bytes(oid_value)?; + + // Parameters are optional and the most common form is a NULL TLV + // (`05 00`). We expose the raw TLV when present so the caller can + // re-parse for non-NULL parameters such as EC named curves. + let parameters = if inner.is_empty() { + None + } else { + let Tlv { raw, tag: t, .. } = inner.read_tlv()?; + // Treat explicit NULL the same as absent — callers don't care. + if t == tag::TAG_NULL { + None + } else { + Some(raw) + } + }; + inner.end()?; + Ok(Self { + algorithm, + parameters, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::der::oid::OID_RSA_ENCRYPTION; + + #[test] + fn rsa_with_null_parameters() { + // SEQUENCE { OID 1.2.840.113549.1.1.1, NULL } + let mut bytes = vec![tag::TAG_SEQUENCE, 0x0d, tag::TAG_OBJECT_IDENTIFIER, 0x09]; + bytes.extend_from_slice(OID_RSA_ENCRYPTION); + bytes.extend_from_slice(&[tag::TAG_NULL, 0x00]); + + let mut r = DerReader::new(&bytes); + let alg = AlgorithmIdentifier::parse(&mut r).unwrap(); + assert_eq!(alg.algorithm.to_id_string(), "1.2.840.113549.1.1.1"); + assert!(alg.parameters.is_none()); + } + + #[test] + fn ec_with_curve_parameters() { + // SEQUENCE { OID id-ecPublicKey, OID secp256r1 } + let alg_oid = crate::der::oid::OID_EC_PUBLIC_KEY; + let curve_oid = crate::der::oid::OID_SECP256R1; + let mut bytes = vec![tag::TAG_SEQUENCE, 0]; + bytes.push(tag::TAG_OBJECT_IDENTIFIER); + bytes.push(alg_oid.len() as u8); + bytes.extend_from_slice(alg_oid); + bytes.push(tag::TAG_OBJECT_IDENTIFIER); + bytes.push(curve_oid.len() as u8); + bytes.extend_from_slice(curve_oid); + let inner_len = bytes.len() - 2; + bytes[1] = inner_len as u8; + + let mut r = DerReader::new(&bytes); + let alg = AlgorithmIdentifier::parse(&mut r).unwrap(); + assert_eq!(alg.algorithm.to_id_string(), "1.2.840.10045.2.1"); + + // The parameters field should hold the curve OID's full TLV. + let params = alg.parameters.expect("EC params present"); + assert_eq!(params[0], tag::TAG_OBJECT_IDENTIFIER); + assert_eq!(params[1] as usize, curve_oid.len()); + assert_eq!(¶ms[2..], curve_oid); + } +} diff --git a/rust_certinfo/src/x509/certificate.rs b/rust_certinfo/src/x509/certificate.rs new file mode 100644 index 0000000..16569e8 --- /dev/null +++ b/rust_certinfo/src/x509/certificate.rs @@ -0,0 +1,132 @@ +// rust_certinfo/src/x509/certificate.rs +// +// Top-level certificate walker. Implements just enough of RFC 5280 §4.1 +// for `certinfo` to expose what it does today via PyO3. +// +// Certificate ::= SEQUENCE { +// tbsCertificate TBSCertificate, +// signatureAlgorithm AlgorithmIdentifier, +// signatureValue BIT STRING +// } +// +// TBSCertificate ::= SEQUENCE { +// version [0] EXPLICIT Version DEFAULT v1, +// serialNumber CertificateSerialNumber, +// signature AlgorithmIdentifier, +// issuer Name, +// validity Validity, +// subject Name, +// subjectPublicKeyInfo SubjectPublicKeyInfo, +// issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, +// subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, +// extensions [3] EXPLICIT Extensions OPTIONAL +// } + +use crate::der::{tag, time, DerReader}; +use crate::error::ParseError; +use crate::x509::{ + algorithm::AlgorithmIdentifier, extensions::Extensions, name::Name, spki::SubjectPublicKeyInfo, +}; + +#[derive(Debug, Clone, Copy)] +pub struct Validity { + pub not_before_unix: i64, + pub not_after_unix: i64, +} + +#[derive(Debug, Clone, Copy)] +pub struct Certificate<'a> { + /// Raw serial number value bytes (without the INTEGER tag/length). + /// Used to render the lowercase-hex "serial_number" Python field. + pub serial_raw: &'a [u8], + /// AlgorithmIdentifier of the **outer** signatureAlgorithm field. + pub signature_algorithm: AlgorithmIdentifier<'a>, + pub issuer: Name<'a>, + pub subject: Name<'a>, + pub validity: Validity, + pub spki: SubjectPublicKeyInfo<'a>, + pub extensions: Extensions<'a>, +} + +impl<'a> Certificate<'a> { + pub fn from_der(der: &'a [u8]) -> Result { + let mut top = DerReader::new(der); + let mut cert_inner = top.expect_constructed(tag::TAG_SEQUENCE)?; + // Whatever the input was, there should be exactly one Certificate. + top.end()?; + + // tbsCertificate + let mut tbs = cert_inner.expect_constructed(tag::TAG_SEQUENCE)?; + // signatureAlgorithm (outer) + let signature_algorithm = AlgorithmIdentifier::parse(&mut cert_inner)?; + // signatureValue (we don't read the value, but we do skip it) + let _sig = cert_inner.read_tlv()?; + cert_inner.end()?; + + // Inside tbsCertificate + // Optional [0] EXPLICIT version + if let Some(tag::CONTEXT_CONSTRUCTED_0) = tbs.peek_tag() { + // Version is INTEGER 0/1/2; skip the whole [0] wrapper since + // we don't need the value. + let _ = tbs.read_tlv()?; + } + // serialNumber + let serial_raw = tbs.expect(tag::TAG_INTEGER)?; + // signature (inner AlgorithmIdentifier — should equal the outer one; + // we don't validate equality, just skip) + let _inner_sig = AlgorithmIdentifier::parse(&mut tbs)?; + let issuer = Name::parse(&mut tbs)?; + let validity = parse_validity(&mut tbs)?; + let subject = Name::parse(&mut tbs)?; + let spki = SubjectPublicKeyInfo::parse(&mut tbs)?; + + // Optional unique IDs and extensions + let mut extensions_body: &'a [u8] = &[]; + while !tbs.is_empty() { + match tbs.peek_tag() { + Some(0x81) => { + // [1] IMPLICIT issuerUniqueID — skip + let _ = tbs.read_tlv()?; + } + Some(0x82) => { + // [2] IMPLICIT subjectUniqueID — skip + let _ = tbs.read_tlv()?; + } + Some(tag::CONTEXT_CONSTRUCTED_3) => { + // [3] EXPLICIT extensions — unwrap once to get the SEQUENCE + let mut ext_wrapper = tbs.expect_constructed(tag::CONTEXT_CONSTRUCTED_3)?; + let inner = ext_wrapper.expect(tag::TAG_SEQUENCE)?; + extensions_body = inner; + ext_wrapper.end()?; + } + _ => { + // Unknown trailing field; advance past it to remain + // tolerant of certs with non-standard trailing data. + let _ = tbs.read_tlv()?; + } + } + } + tbs.end()?; + + Ok(Certificate { + serial_raw, + signature_algorithm, + issuer, + subject, + validity, + spki, + extensions: Extensions::from_body(extensions_body), + }) + } +} + +fn parse_validity(reader: &mut DerReader<'_>) -> Result { + let mut inner = reader.expect_constructed(tag::TAG_SEQUENCE)?; + let nb_tlv = inner.read_tlv()?; + let na_tlv = inner.read_tlv()?; + inner.end()?; + Ok(Validity { + not_before_unix: time::parse_time(nb_tlv.tag, nb_tlv.value)?, + not_after_unix: time::parse_time(na_tlv.tag, na_tlv.value)?, + }) +} diff --git a/rust_certinfo/src/x509/extensions.rs b/rust_certinfo/src/x509/extensions.rs new file mode 100644 index 0000000..8177315 --- /dev/null +++ b/rust_certinfo/src/x509/extensions.rs @@ -0,0 +1,273 @@ +// rust_certinfo/src/x509/extensions.rs +// +// Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension +// Extension ::= SEQUENCE { +// extnID OBJECT IDENTIFIER, +// critical BOOLEAN DEFAULT FALSE, +// extnValue OCTET STRING -- contains DER-encoded extension type +// } +// +// We only parse three extensions today: BasicConstraints, SKI, AKI. +// Adding a new extension is a single accessor on `Extensions` plus a +// matching parser function — no changes to the parent walker required. + +use crate::der::{oid, tag, DerReader, Oid}; +use crate::error::ParseError; + +#[derive(Debug, Clone, Copy)] +pub struct Extensions<'a> { + body: &'a [u8], +} + +#[derive(Debug, Clone, Copy)] +pub struct Extension<'a> { + pub oid: Oid<'a>, + /// `true` if the extension was marked critical. Not surfaced to Python + /// today, but available to in-tree future extension parsers. + #[allow(dead_code)] + pub critical: bool, + /// Inner DER bytes after unwrapping the OCTET STRING. + pub value: &'a [u8], +} + +#[derive(Debug, Clone, Copy)] +pub struct BasicConstraints { + pub ca: bool, + pub path_len: Option, +} + +#[derive(Debug, Clone, Copy)] +pub struct AuthorityKeyIdentifier<'a> { + pub key_identifier: Option<&'a [u8]>, +} + +impl<'a> Extensions<'a> { + /// Build an Extensions wrapper from the contents of a SEQUENCE OF + /// Extension, i.e. the value bytes of the outer SEQUENCE. + pub fn from_body(body: &'a [u8]) -> Self { + Self { body } + } + + /// Iterate all extensions in document order. + pub fn iter(&self) -> ExtensionIter<'a> { + ExtensionIter { + reader: DerReader::new(self.body), + } + } + + /// Find a single extension by raw OID bytes. Returns the first match. + fn find(&self, oid_bytes: &[u8]) -> Result>, ParseError> { + for ext in self.iter() { + let ext = ext?; + if ext.oid.as_bytes() == oid_bytes { + return Ok(Some(ext)); + } + } + Ok(None) + } + + pub fn basic_constraints(&self) -> Result, ParseError> { + let Some(ext) = self.find(oid::OID_EXT_BASIC_CONSTRAINTS)? else { + return Ok(None); + }; + Ok(Some(parse_basic_constraints(ext.value)?)) + } + + pub fn subject_key_identifier(&self) -> Result, ParseError> { + let Some(ext) = self.find(oid::OID_EXT_SKI)? else { + return Ok(None); + }; + // SubjectKeyIdentifier ::= KeyIdentifier + // KeyIdentifier ::= OCTET STRING + let mut r = DerReader::new(ext.value); + let value = r.expect(tag::TAG_OCTET_STRING)?; + r.end()?; + Ok(Some(value)) + } + + pub fn authority_key_identifier( + &self, + ) -> Result>, ParseError> { + let Some(ext) = self.find(oid::OID_EXT_AKI)? else { + return Ok(None); + }; + Ok(Some(parse_authority_key_identifier(ext.value)?)) + } +} + +pub struct ExtensionIter<'a> { + reader: DerReader<'a>, +} + +impl<'a> Iterator for ExtensionIter<'a> { + type Item = Result, ParseError>; + + fn next(&mut self) -> Option { + self.reader.peek_tag()?; + let mut inner = match self.reader.expect_constructed(tag::TAG_SEQUENCE) { + Ok(r) => r, + Err(e) => return Some(Err(e)), + }; + let oid_value = match inner.expect(tag::TAG_OBJECT_IDENTIFIER) { + Ok(v) => v, + Err(e) => return Some(Err(e)), + }; + let oid = match Oid::from_bytes(oid_value) { + Ok(o) => o, + Err(e) => return Some(Err(e)), + }; + + // critical BOOLEAN DEFAULT FALSE — present iff the next tag is + // 0x01 (BOOLEAN), otherwise absent and the default applies. + let critical = match inner.peek_tag() { + Some(tag::TAG_BOOLEAN) => { + let value = match inner.expect(tag::TAG_BOOLEAN) { + Ok(v) => v, + Err(e) => return Some(Err(e)), + }; + value.first().copied().unwrap_or(0) != 0 + } + _ => false, + }; + let value = match inner.expect(tag::TAG_OCTET_STRING) { + Ok(v) => v, + Err(e) => return Some(Err(e)), + }; + if let Err(e) = inner.end() { + return Some(Err(e)); + } + Some(Ok(Extension { + oid, + critical, + value, + })) + } +} + +fn parse_basic_constraints(value: &[u8]) -> Result { + // BasicConstraints ::= SEQUENCE { + // cA BOOLEAN DEFAULT FALSE, + // pathLenConstraint INTEGER (0..MAX) OPTIONAL + // } + let mut r = DerReader::new(value); + let mut inner = r.expect_constructed(tag::TAG_SEQUENCE)?; + let mut bc = BasicConstraints { + ca: false, + path_len: None, + }; + if let Some(tag::TAG_BOOLEAN) = inner.peek_tag() { + let val = inner.expect(tag::TAG_BOOLEAN)?; + bc.ca = val.first().copied().unwrap_or(0) != 0; + } + if let Some(tag::TAG_INTEGER) = inner.peek_tag() { + let val = inner.expect(tag::TAG_INTEGER)?; + // Decode small unsigned integer; cert path length is always small. + let n = val + .iter() + .try_fold(0u64, |acc, &b| { + acc.checked_shl(8).and_then(|v| v.checked_add(b as u64)) + }) + .ok_or(ParseError::IntegerOverflow)?; + bc.path_len = Some(n.try_into().map_err(|_| ParseError::IntegerOverflow)?); + } + inner.end()?; + r.end()?; + Ok(bc) +} + +fn parse_authority_key_identifier(value: &[u8]) -> Result, ParseError> { + // AuthorityKeyIdentifier ::= SEQUENCE { + // keyIdentifier [0] OCTET STRING OPTIONAL, + // authorityCertIssuer [1] GeneralNames OPTIONAL, + // authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL + // } + // + // We only care about [0] (the key identifier). The IMPLICIT tagging + // means the wire form is `0x80 || length || octet_string_bytes`. + let mut r = DerReader::new(value); + let mut inner = r.expect_constructed(tag::TAG_SEQUENCE)?; + let mut key_identifier = None; + while inner.peek_tag().is_some() { + let tlv = inner.read_tlv()?; + if tlv.tag == 0x80 { + // [0] IMPLICIT OCTET STRING — value is the raw key identifier. + key_identifier = Some(tlv.value); + } + // [1] and [2] are skipped — we don't surface them today. + } + r.end()?; + Ok(AuthorityKeyIdentifier { key_identifier }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic_constraints_ca_true() { + // BasicConstraints { cA TRUE } extension value (the OCTET STRING contents) + // SEQUENCE { BOOLEAN TRUE } = 30 03 01 01 FF + let value = [tag::TAG_SEQUENCE, 0x03, tag::TAG_BOOLEAN, 0x01, 0xff]; + let bc = parse_basic_constraints(&value).unwrap(); + assert!(bc.ca); + assert_eq!(bc.path_len, None); + } + + #[test] + fn basic_constraints_default_false() { + // SEQUENCE {} = 30 00 + let value = [tag::TAG_SEQUENCE, 0x00]; + let bc = parse_basic_constraints(&value).unwrap(); + assert!(!bc.ca); + assert_eq!(bc.path_len, None); + } + + #[test] + fn basic_constraints_with_path_len() { + // SEQUENCE { BOOLEAN TRUE, INTEGER 3 } + let value = [ + tag::TAG_SEQUENCE, + 0x06, + tag::TAG_BOOLEAN, + 0x01, + 0xff, + tag::TAG_INTEGER, + 0x01, + 0x03, + ]; + let bc = parse_basic_constraints(&value).unwrap(); + assert!(bc.ca); + assert_eq!(bc.path_len, Some(3)); + } + + #[test] + fn aki_key_identifier_only() { + // SEQUENCE { [0] IMPLICIT OCTET STRING (20 bytes) } + let mut value = vec![tag::TAG_SEQUENCE, 0x16, 0x80, 0x14]; + value.extend(vec![0xab; 20]); + let aki = parse_authority_key_identifier(&value).unwrap(); + assert_eq!(aki.key_identifier.unwrap().len(), 20); + assert!(aki.key_identifier.unwrap().iter().all(|&b| b == 0xab)); + } + + #[test] + fn aki_with_extra_fields_ignored() { + // SEQUENCE { [0] OCTET STRING (4 bytes), [1] (2 bytes) } + let value = [ + tag::TAG_SEQUENCE, + 0x0a, + 0x80, + 0x04, + 0x01, + 0x02, + 0x03, + 0x04, + 0xa1, + 0x02, + 0xff, + 0xff, + ]; + let aki = parse_authority_key_identifier(&value).unwrap(); + assert_eq!(aki.key_identifier, Some(&[0x01, 0x02, 0x03, 0x04][..])); + } +} diff --git a/rust_certinfo/src/x509/name.rs b/rust_certinfo/src/x509/name.rs new file mode 100644 index 0000000..7ceb30b --- /dev/null +++ b/rust_certinfo/src/x509/name.rs @@ -0,0 +1,255 @@ +// rust_certinfo/src/x509/name.rs +// +// Name ::= CHOICE { rdnSequence RDNSequence } +// RDNSequence ::= SEQUENCE OF RelativeDistinguishedName +// RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue +// AttributeTypeAndValue ::= SEQUENCE { type OID, value ANY } +// +// `Name` keeps the raw outer SEQUENCE bytes for byte-equality comparison +// (which is what we use to verify chain-link parent.subject == child.issuer). +// The attribute accessors (`common_name`, `organization`, etc.) walk the +// nested SEQUENCE/SET/SEQUENCE structure on demand. + +use crate::der::{string, tag, DerReader, Oid}; +use crate::error::ParseError; + +#[derive(Debug, Clone, Copy)] +pub struct Name<'a> { + /// Outer Name TLV including the SEQUENCE tag and length prefix. Used by + /// `chain_analysis` for canonical DN equality comparison. + pub raw: &'a [u8], + body: &'a [u8], +} + +impl<'a> Name<'a> { + /// Parse a Name from a sub-reader positioned at its outer SEQUENCE tag. + /// Advances the reader past the entire Name. + pub fn parse(reader: &mut DerReader<'a>) -> Result { + let tlv = reader.read_tlv()?; + if tlv.tag != tag::TAG_SEQUENCE { + return Err(ParseError::UnexpectedTag { + expected: tag::TAG_SEQUENCE, + got: tlv.tag, + }); + } + Ok(Self { + raw: tlv.raw, + body: tlv.value, + }) + } + + pub fn common_name(&self) -> Option { + self.first_value_of(crate::der::oid::OID_AT_COMMON_NAME) + } + + pub fn organization(&self) -> Option { + self.first_value_of(crate::der::oid::OID_AT_ORGANIZATION_NAME) + } + + pub fn organizational_unit(&self) -> Option { + self.first_value_of(crate::der::oid::OID_AT_ORGANIZATIONAL_UNIT_NAME) + } + + pub fn country(&self) -> Option { + self.first_value_of(crate::der::oid::OID_AT_COUNTRY_NAME) + } + + /// Return the first AttributeTypeAndValue whose type matches `attr_oid_bytes`. + /// Stops at the first match, mirroring `x509-parser`'s `iter_*().next()` calls. + fn first_value_of(&self, attr_oid_bytes: &[u8]) -> Option { + let mut rdns = DerReader::new(self.body); + while rdns.peek_tag().is_some() { + let rdn_inner = rdns.expect_constructed(tag::TAG_SET).ok()?; + // Walk every ATV in the RDN set + let mut atvs = rdn_inner; + while atvs.peek_tag().is_some() { + let mut atv = atvs.expect_constructed(tag::TAG_SEQUENCE).ok()?; + let oid_bytes = atv.expect(tag::TAG_OBJECT_IDENTIFIER).ok()?; + if oid_bytes == attr_oid_bytes { + let value_tlv = atv.read_tlv().ok()?; + return string::parse_string(value_tlv.tag, value_tlv.value).ok(); + } + } + } + None + } + + /// Iterate every (type OID, decoded value) pair in document order. + /// Used by future extension code that needs more than the four + /// well-known fields above. + #[allow(dead_code)] + pub fn iter_attributes(&self) -> NameAttrIter<'a> { + NameAttrIter { + rdns: DerReader::new(self.body), + current_rdn: None, + } + } +} + +#[allow(dead_code)] +pub struct NameAttrIter<'a> { + rdns: DerReader<'a>, + current_rdn: Option>, +} + +impl<'a> Iterator for NameAttrIter<'a> { + type Item = Result<(Oid<'a>, String), ParseError>; + + fn next(&mut self) -> Option { + loop { + // If we're inside an RDN, try to read the next ATV. + if let Some(rdn) = self.current_rdn.as_mut() { + if rdn.peek_tag().is_some() { + let mut atv = match rdn.expect_constructed(tag::TAG_SEQUENCE) { + Ok(r) => r, + Err(e) => return Some(Err(e)), + }; + let oid_value = match atv.expect(tag::TAG_OBJECT_IDENTIFIER) { + Ok(v) => v, + Err(e) => return Some(Err(e)), + }; + let oid = match Oid::from_bytes(oid_value) { + Ok(o) => o, + Err(e) => return Some(Err(e)), + }; + let value_tlv = match atv.read_tlv() { + Ok(t) => t, + Err(e) => return Some(Err(e)), + }; + let value = match string::parse_string(value_tlv.tag, value_tlv.value) { + Ok(s) => s, + Err(e) => return Some(Err(e)), + }; + return Some(Ok((oid, value))); + } + self.current_rdn = None; + } + // Need to advance to the next RDN. + self.rdns.peek_tag()?; + let next_rdn = match self.rdns.expect_constructed(tag::TAG_SET) { + Ok(r) => r, + Err(e) => return Some(Err(e)), + }; + self.current_rdn = Some(next_rdn); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Build a Name DER for `CN=example.com, O=Example Inc, C=US`. + fn sample_name_der() -> Vec { + // The DER for this is hand-built byte-by-byte to keep the test + // self-contained. + // RDN 1: SET { SEQUENCE { OID 2.5.4.6 (countryName), PrintableString "US" } } + let rdn_c = vec![ + tag::TAG_SET, + 0x0b, + tag::TAG_SEQUENCE, + 0x09, + tag::TAG_OBJECT_IDENTIFIER, + 0x03, + 0x55, + 0x04, + 0x06, + tag::TAG_PRINTABLE_STRING, + 0x02, + b'U', + b'S', + ]; + // RDN 2: SET { SEQUENCE { OID 2.5.4.10 (organizationName), UTF8String "Example Inc" } } + let rdn_o = vec![ + tag::TAG_SET, + 0x14, + tag::TAG_SEQUENCE, + 0x12, + tag::TAG_OBJECT_IDENTIFIER, + 0x03, + 0x55, + 0x04, + 0x0a, + tag::TAG_UTF8_STRING, + 0x0b, + b'E', + b'x', + b'a', + b'm', + b'p', + b'l', + b'e', + b' ', + b'I', + b'n', + b'c', + ]; + // RDN 3: SET { SEQUENCE { OID 2.5.4.3 (commonName), UTF8String "example.com" } } + let rdn_cn = vec![ + tag::TAG_SET, + 0x14, + tag::TAG_SEQUENCE, + 0x12, + tag::TAG_OBJECT_IDENTIFIER, + 0x03, + 0x55, + 0x04, + 0x03, + tag::TAG_UTF8_STRING, + 0x0b, + b'e', + b'x', + b'a', + b'm', + b'p', + b'l', + b'e', + b'.', + b'c', + b'o', + b'm', + ]; + + let mut body: Vec = Vec::new(); + body.extend(&rdn_c); + body.extend(&rdn_o); + body.extend(&rdn_cn); + + let mut out = vec![tag::TAG_SEQUENCE, body.len() as u8]; + out.extend(body); + out + } + + #[test] + fn parses_attributes() { + let bytes = sample_name_der(); + let mut r = DerReader::new(&bytes); + let name = Name::parse(&mut r).unwrap(); + assert_eq!(name.common_name().as_deref(), Some("example.com")); + assert_eq!(name.organization().as_deref(), Some("Example Inc")); + assert_eq!(name.country().as_deref(), Some("US")); + assert_eq!(name.organizational_unit(), None); + } + + #[test] + fn raw_includes_outer_sequence() { + let bytes = sample_name_der(); + let mut r = DerReader::new(&bytes); + let name = Name::parse(&mut r).unwrap(); + assert_eq!(name.raw, bytes.as_slice()); + } + + #[test] + fn iter_attributes_visits_all() { + let bytes = sample_name_der(); + let mut r = DerReader::new(&bytes); + let name = Name::parse(&mut r).unwrap(); + let attrs: Vec<(String, String)> = name + .iter_attributes() + .filter_map(Result::ok) + .map(|(oid, val)| (oid.to_id_string(), val)) + .collect(); + assert_eq!(attrs.len(), 3); + assert_eq!(attrs[0], ("2.5.4.6".to_string(), "US".to_string())); + } +} diff --git a/rust_certinfo/src/x509/spki.rs b/rust_certinfo/src/x509/spki.rs new file mode 100644 index 0000000..f2db4be --- /dev/null +++ b/rust_certinfo/src/x509/spki.rs @@ -0,0 +1,281 @@ +// rust_certinfo/src/x509/spki.rs +// +// SubjectPublicKeyInfo ::= SEQUENCE { +// algorithm AlgorithmIdentifier, +// subjectPublicKey BIT STRING +// } +// +// Two key types matter for the public web today: RSA and EC. For RSA we +// extract the modulus bit length from the inner SubjectPublicKey contents. +// For EC we extract the curve OID from `algorithm.parameters` (the +// **previous** code used `algorithm.oid()` here by mistake — that's +// id-ecPublicKey, not the curve — and this rewrite is the place we fix +// the bug). Other key types collapse to `Unknown`, matching the prior +// behavior so the Python-facing dict shape doesn't change. + +use crate::der::{oid, tag, DerReader, Oid}; +use crate::error::ParseError; +use crate::x509::algorithm::AlgorithmIdentifier; + +#[derive(Debug, Clone, Copy)] +pub struct SubjectPublicKeyInfo<'a> { + pub algorithm: AlgorithmIdentifier<'a>, + /// BIT STRING contents *after* the unused-bits prefix byte. For RSA + /// this wraps `RSAPublicKey ::= SEQUENCE { modulus, publicExponent }`. + /// For EC this is the raw EC point. + pub subject_public_key: &'a [u8], + /// Outer SubjectPublicKeyInfo TLV including the SEQUENCE tag and + /// length prefix. This is what `extract_public_key_der` returns. + pub raw: &'a [u8], +} + +#[derive(Debug, Clone)] +pub enum PublicKeyAlgorithm<'a> { + Rsa { modulus_bits: usize }, + Ec { curve_oid: Oid<'a>, key_bits: usize }, + Unknown, +} + +impl<'a> SubjectPublicKeyInfo<'a> { + /// Parse a SubjectPublicKeyInfo from a sub-reader positioned at its + /// outer SEQUENCE tag. Captures the raw outer TLV slice for later + /// retrieval via `extract_public_key_der`. + pub fn parse(reader: &mut DerReader<'a>) -> Result { + let tlv = reader.read_tlv()?; + if tlv.tag != tag::TAG_SEQUENCE { + return Err(ParseError::UnexpectedTag { + expected: tag::TAG_SEQUENCE, + got: tlv.tag, + }); + } + let raw = tlv.raw; + let mut inner = DerReader::new(tlv.value); + let algorithm = AlgorithmIdentifier::parse(&mut inner)?; + + let bit_string_value = inner.expect(tag::TAG_BIT_STRING)?; + if bit_string_value.is_empty() { + return Err(ParseError::InvalidBitString); + } + // First byte of a BIT STRING is the count of unused trailing bits; + // for SPKI it's always 0. + if bit_string_value[0] != 0 { + return Err(ParseError::InvalidBitString); + } + let subject_public_key = &bit_string_value[1..]; + + inner.end()?; + Ok(Self { + algorithm, + subject_public_key, + raw, + }) + } + + pub fn parsed(&self) -> PublicKeyAlgorithm<'a> { + let alg_bytes = self.algorithm.algorithm.as_bytes(); + if alg_bytes == oid::OID_RSA_ENCRYPTION { + return parse_rsa(self.subject_public_key); + } + if alg_bytes == oid::OID_EC_PUBLIC_KEY { + return parse_ec(self); + } + PublicKeyAlgorithm::Unknown + } +} + +/// RSA SubjectPublicKey: `SEQUENCE { modulus INTEGER, publicExponent INTEGER }`. +/// We compute the modulus bit length the same way x509-parser does: +/// `modulus_bytes.len() * 8`, where `modulus_bytes` is the INTEGER value +/// **excluding** any leading 0x00 byte that DER inserts to keep the value +/// unsigned. +fn parse_rsa(subject_public_key: &[u8]) -> PublicKeyAlgorithm<'static> { + let mut r = DerReader::new(subject_public_key); + let inner = match r.expect_constructed(tag::TAG_SEQUENCE) { + Ok(s) => s, + Err(_) => return PublicKeyAlgorithm::Unknown, + }; + let mut inner = inner; + let modulus = match inner.expect(tag::TAG_INTEGER) { + Ok(v) => v, + Err(_) => return PublicKeyAlgorithm::Unknown, + }; + // DER unsigned integers prepend 0x00 if the high bit is set; strip it + // so we report the true bit length. x509-parser does the equivalent. + let trimmed = if modulus.len() > 1 && modulus[0] == 0x00 { + &modulus[1..] + } else { + modulus + }; + PublicKeyAlgorithm::Rsa { + modulus_bits: trimmed.len() * 8, + } +} + +/// EC SubjectPublicKey: +/// - `algorithm.parameters` is an ECParameters CHOICE; in practice it's +/// always a named-curve OID. +/// - `subjectPublicKey` is the encoded point (uncompressed `0x04 || X || Y`, +/// or compressed `0x02 / 0x03 || X`). +fn parse_ec<'a>(spki: &SubjectPublicKeyInfo<'a>) -> PublicKeyAlgorithm<'a> { + let curve_oid = match spki.algorithm.parameters { + Some(raw) => match parse_oid_tlv(raw) { + Some(o) => o, + None => return PublicKeyAlgorithm::Unknown, + }, + None => return PublicKeyAlgorithm::Unknown, + }; + let key_bits = ec_key_bits(curve_oid, spki.subject_public_key); + PublicKeyAlgorithm::Ec { + curve_oid, + key_bits, + } +} + +/// Extract an `Oid` from a raw OID TLV (tag + length + value). +fn parse_oid_tlv(raw: &[u8]) -> Option> { + let mut r = DerReader::new(raw); + let value = r.expect(tag::TAG_OBJECT_IDENTIFIER).ok()?; + Oid::from_bytes(value).ok() +} + +/// Map a curve OID to its field bit length, or fall back to computing it +/// from the EC point byte length when the curve is not in our table. +/// Uncompressed point: `0x04 || X || Y` → field bytes = (len - 1) / 2. +/// Compressed point: `0x02 / 0x03 || X` → field bytes = len - 1. +fn ec_key_bits(curve_oid: Oid<'_>, subject_public_key: &[u8]) -> usize { + let bytes = curve_oid.as_bytes(); + if bytes == oid::OID_SECP256R1 { + return 256; + } + if bytes == oid::OID_SECP384R1 { + return 384; + } + if bytes == oid::OID_SECP521R1 { + return 521; + } + if bytes == oid::OID_SECP256K1 { + return 256; + } + // Fallback: derive from the raw point. + if subject_public_key.is_empty() { + return 0; + } + let leading = subject_public_key[0]; + let payload = &subject_public_key[1..]; + let field_bytes = match leading { + 0x04 => payload.len() / 2, + 0x02 | 0x03 => payload.len(), + _ => return 0, + }; + field_bytes * 8 +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Build a minimal SPKI for RSA-2048 with all-zero modulus (still + /// 256 bytes long, which is what we measure). + fn rsa_2048_spki() -> Vec { + // RSAPublicKey: SEQUENCE { INTEGER modulus, INTEGER exponent } + // modulus: 257 bytes (0x00 || 256 zero bytes) so the trimmed + // length is 256 bytes = 2048 bits. + let mut modulus = vec![0x00u8]; + modulus.extend(vec![0u8; 256]); + // SEQUENCE { INTEGER modulus, INTEGER publicExponent } + let mut rsa_pk = vec![ + tag::TAG_INTEGER, + 0x82, + ((modulus.len() >> 8) & 0xff) as u8, + (modulus.len() & 0xff) as u8, + ]; + rsa_pk.extend(&modulus); + // INTEGER publicExponent (0x010001 = 65537) + rsa_pk.extend(&[tag::TAG_INTEGER, 0x03, 0x01, 0x00, 0x01]); + + let mut sequence = vec![ + tag::TAG_SEQUENCE, + 0x82, + ((rsa_pk.len() >> 8) & 0xff) as u8, + (rsa_pk.len() & 0xff) as u8, + ]; + sequence.extend(&rsa_pk); + + // BIT STRING wrapper: unused-bits byte + RSA SEQUENCE + let mut bit_string = vec![tag::TAG_BIT_STRING]; + let bs_len = sequence.len() + 1; + bit_string.push(0x82); + bit_string.push(((bs_len >> 8) & 0xff) as u8); + bit_string.push((bs_len & 0xff) as u8); + bit_string.push(0x00); // unused bits + bit_string.extend(&sequence); + + // AlgorithmIdentifier { OID rsaEncryption, NULL } + let alg_bytes = oid::OID_RSA_ENCRYPTION; + let mut alg = vec![tag::TAG_SEQUENCE, 0x0d]; + alg.push(tag::TAG_OBJECT_IDENTIFIER); + alg.push(alg_bytes.len() as u8); + alg.extend(alg_bytes); + alg.extend(&[tag::TAG_NULL, 0x00]); + + // Outer SubjectPublicKeyInfo SEQUENCE + let inner_len = alg.len() + bit_string.len(); + let mut spki = vec![tag::TAG_SEQUENCE, 0x82]; + spki.push(((inner_len >> 8) & 0xff) as u8); + spki.push((inner_len & 0xff) as u8); + spki.extend(&alg); + spki.extend(&bit_string); + spki + } + + #[test] + fn rsa_modulus_bit_length() { + let bytes = rsa_2048_spki(); + let mut r = DerReader::new(&bytes); + let spki = SubjectPublicKeyInfo::parse(&mut r).unwrap(); + match spki.parsed() { + PublicKeyAlgorithm::Rsa { modulus_bits } => assert_eq!(modulus_bits, 2048), + other => panic!("expected RSA, got {:?}", other), + } + } + + #[test] + fn ec_curve_bit_length_p256() { + // AlgorithmIdentifier { OID id-ecPublicKey, OID secp256r1 } + let alg_oid = oid::OID_EC_PUBLIC_KEY; + let curve_oid = oid::OID_SECP256R1; + let mut alg_inner = Vec::new(); + alg_inner.push(tag::TAG_OBJECT_IDENTIFIER); + alg_inner.push(alg_oid.len() as u8); + alg_inner.extend(alg_oid); + alg_inner.push(tag::TAG_OBJECT_IDENTIFIER); + alg_inner.push(curve_oid.len() as u8); + alg_inner.extend(curve_oid); + let mut alg = vec![tag::TAG_SEQUENCE, alg_inner.len() as u8]; + alg.extend(&alg_inner); + + // Uncompressed P-256 point: 0x04 || 32 bytes X || 32 bytes Y = 65 bytes. + let mut bs = vec![tag::TAG_BIT_STRING, 66]; + bs.push(0x00); + bs.push(0x04); + bs.extend(vec![0u8; 64]); + + let inner_len = alg.len() + bs.len(); + let mut spki = vec![tag::TAG_SEQUENCE, inner_len as u8]; + spki.extend(&alg); + spki.extend(&bs); + + let mut r = DerReader::new(&spki); + let spki = SubjectPublicKeyInfo::parse(&mut r).unwrap(); + match spki.parsed() { + PublicKeyAlgorithm::Ec { + curve_oid, + key_bits, + } => { + assert_eq!(curve_oid.to_id_string(), "1.2.840.10045.3.1.7"); + assert_eq!(key_bits, 256); + } + other => panic!("expected EC, got {:?}", other), + } + } +} diff --git a/tests/fixtures/diff_corpus/01e4587189b66739.der b/tests/fixtures/diff_corpus/01e4587189b66739.der new file mode 100644 index 0000000000000000000000000000000000000000..a7353dd69747bc410b77fb98309c6b9d6a020f36 GIT binary patch literal 1930 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 KEhh?M8vh5=@wOBI literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/0587d6bd2819587a.der b/tests/fixtures/diff_corpus/0587d6bd2819587a.der new file mode 100644 index 0000000000000000000000000000000000000000..0330cc5ab0dc5eed8ff156db316697a6708b4d5a GIT binary patch literal 893 zcmXqLVy-l3V*0;;nTe5!Nr0Q-rLJ#ovHU56$m6HwB-#wP*f_M>JkHs&Ff$t@8gd(O zvN4CUun9AT1{;bR2!l9W!aOdS>6y-{MI{QJdC7*72I3%LZefA)@^ZZtpj0wYN-sG- z*HFPg4kX1aEDqPt+9%zPO38$)Jg`&Oiw0WLaTG#{VoF25dlziIKrT79_^UBE}-prTltc_Vu6Uu|IPa zEz4#tOur|v!ayD*t;`}}Al4wV`L@TBp9_!PSZC&VMld-uNpp|)F#|r30)CL0EWr3} zgP5nvB4!}M#-Yu|$jZvj%m`;O8I-};jEpQ<1{nq_Funm}n?y!QNr9EVetvRs0ZObI zIKWjgwV6OwB`1O+y%&F zOq3Zp2DTtG6j;m*Oboc$I1}1D7~B5lFfkgiv$3W(@-Q+QaImp~7(gKyiwVwRMo#C< z84L!gOa^so5qa-A-tgQNb$nN~W9H0)i%F^e3l!$LZ;U>;Dcw-^Oi|T(r6LEXywsL| z?cV&7Ooj~4_Fr3P(jIM}eq44%{#Buht;9}#@YV$Z}%fifLU~R~4 zz{$oO%EBhh6dG(OXCMROa0!dL=jW&Aq$-3Il@^yM1g932WhSQ<8}a}(1Lc{8nZsQT z4dldmjm!*;4J`}|O$>|;qQG1;Lo)*-69Xf&CXY#K|DSdF&2>xt5M*_mW1DnFNlAf~zJ9)5L3XBIIxv9si}lM>4fTsdlMPhi3Ypp@p$an*3W3V> zl8Z_V^g#{~WYI8CYr+W9CLt|7j9>=)Spno{0}eK}^hO>=CPo7@kSX#k`UbiN+6y!n zsJE#?ZA?b7QQyNdDaFgF)G(wh9cX)wK@%e@8y7I-SQxKWGO;kRG=5`g{LJu?p^Skq ze9E5B>sHzsOY6*J-~4^T{n>|X3yoCoDy~#rlX%omk%576&c6%q)-W)zFf+ItxH2i| zzEllL$YJ<@UrzMjpPcnAZ*Mc!=l^~D^~B$0hP|>Ad6*O#?lJ|L)yPcYS(xG{+yCgy zlTCBPT21)XavXTBO8uR2s0fe2wi}bd{#OpVQB!b>+u=m*zIv$-niK Kg2TmlHv#~`2{@$y literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/0780fe78243e6b0c.der b/tests/fixtures/diff_corpus/0780fe78243e6b0c.der new file mode 100644 index 0000000000000000000000000000000000000000..a48ca509cccf8d16f6ce9544a114996ec861de99 GIT binary patch literal 2082 zcmZuy2~ZPB6rGt&f)MV+aESy_jwnMX+#PbIKQ6Ylk+fTJ8R%6k`l!%zZM6x8VJ+LFEC=4b)PM}ta+5AP} z?6B}4c5o1T9v4isGU53E9?utOe^J`dpXpi5I!k|eQ`Obc*$ zl76W$6{6s<8ARYl2oA$g&xNl|FKFBQzVKs)(wAfLXRBW1cvVmNkiBAadv{U1Yss7J zH^qy@xt?Ja^IyNp>o{cCU$x$a*&>|g4r(KJ&hGlDi`Lgyj(1GXbWyGm%D{L=zSjg zCTrOVr^Q=Xhc92f2rVqr z;=q2C6&2I6_|8*jbir23J=BBC4AL&up4rPcxjC?LX8*I>n$&fLy@lHb+v_z31sj%T z>^SS~^mut*mWa z3FM*2$&$p;;zUWZSdoJ1ofJNvg9+pkELxmA3bq)Azr$3jVn;C<988!fi&c&St#sg6 zwh2ckMcxx!FO-T^IHCHTLZo7~asB(Dk^^#$I*lSYf( zW(*@Ra04#1aZ_n9m1=;%EW0?BN*LG!qnNRbHSxExEA>)=>EBpLCZ&d9FToEUnfxq(Z9n@G?oA>$QFV8Sv)dVbt)1RBzH3f-UJ+q=*xPcNsyu#C9{oXD+5%Za zKf-1jvyaW{e-e0nb>FUxX}xQFOg`ED5%%2GzH!|denZ7RLU#X;!8<;^s_Na=XEWKk z%vucB-CpB|z_S#wsS&=IO~;7eNB_$@#L+E1-UJMkW>p_(E$a$#*?rV?FmuMkz4iC_ zU8YU;2;0B&^XpEVYC~rJ)j1Mx=M#l>ZnEuMTYGm|!Qkf0@H_oCgm~_VHw1<*+`Z^= z)$S{Q)6>Sg1&4hCuMfOc4^1t%?RTsyF}K^mJKpVDTKnniKX11Seh$YH-rriRDtb?w zQSNNl)UaI=)aD-2<=YZk6_n8E7V){UA4Ep{)fsN>HT4}n%aZMdC!yPUT`9$efwOXJOY*9pdxZ&O zuHAi+5Sr1HEBO5}uZ8RQTSdSHms3%SjvWqzmtS5_9q2ph(AeR#?m+31OnS-38`u8> DT_Xr` literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/092ac16e8faed8a1.der b/tests/fixtures/diff_corpus/092ac16e8faed8a1.der new file mode 100644 index 0000000000000000000000000000000000000000..1fc6e785a4bb5dbaa0184709fc6687f8e059be08 GIT binary patch literal 1640 zcmZ{kdoSj2IEdJHsG$atkAqB`K@oOoPEr4KrhgE@G;Yl10+2 zOGM=;h1HhbEGl-_?O4ic&x+huTJ758*2?aTZq9c4<2~nn-sgEg-{*ba=R81+iv(gU zqZEU{2%-VEYDz~LSG<}Ik4#IXuFu}`%Kmimg zA&3Hu^;9WTKm|?|vV%RD940dzfg^BUF+mu>leK`x3Q1$ITk+uoGi^O_ss#<(Qdk{1r&Vv%R|j!la++KgaamvF-I*Gu=|?AJ zsF&5;wkIxk*7TReyv$|z96n~d$9k92D+O$d0nx43yFzM}_H@H&(D5p8Q<@uC1pv(bJ`rU zf5?lNz?fUlcA?rEP!;E^eJolX<|=LA)=2H81C0>@H*6nAr;5@`AGn;&btXMOpkm?a z8W(PVyJZT01i`=%TrS3NfEW`4Ol9>m(nQw+eewGmiq~u)y)USmJVYIa|3y&@f-MC` za+`?i0z6L98V6&s3K;m^4tecXLv>NSf9D}?!L6LKQ$>$yPPIqHQj(S>*eSnFKm$Pl zP{iTbX(})R15cc?jD*6a6lE9#9027l4}onRe8yU&gn z70>0z3b?#vJD?&5Ys=F_00h#`zeT_>nwA67vVi;B697vb55zy~pfq>0nKo+v+SdhoZ5ajOmw{Kn%Eflyo`Dj!EMyqyeBG^{m5ceC zkh(o%OHF4RE_h!tJJ5D<-}(^G5`Rbt?cns43mcCPje6cpY$-A`msZ4ncTe1L)vrAL za6z3d{91VI_KL7C@Rm`VkNm3lqGPFr{j68*f~DKE=ir{;g~sd`)Of>L^|P;B5kg{g zn&~0Wf|o{C>Fp-OXZ1RM9Ep#RrAfN}W5q~SHeU8%wVinlGuJCO*&Et?rBFAYJydv? zmOvpsGOZ+5FH|hpu zR?*7+q2}UdZl<5AQq>xmiz9lmd-E<3P@gCf8jFMI9yX%!NwY! zcd8&)_>C(AF|M^?=Hw@8arGW9m0gj{h9BEoJJof|`?RNZ^`G`s5f*XS#7D>>la8XL0 zsc128;@)`BfK}3o-Yhq$3V-Hi=p7~6F2{kAK-C{!@FM$0&v6 sCKe+jjBd~dX-qGQ?nnF66FUp1TLY_AGY*(ptu;@25pg%G#kYCzKjW!jT>t<8 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/0a05f2e1bec8c474.der b/tests/fixtures/diff_corpus/0a05f2e1bec8c474.der new file mode 100644 index 0000000000000000000000000000000000000000..e33655e28566c0627213cab6791f1663ba0afcdb GIT binary patch literal 922 zcmXqLVxDHu#4NLbnTe5!Nr*N3Xi)m~V|xl`{xej0(d)8b)qsnQL#xf>oGlA8vw@Kz zw*e;`b0`a&FjHu-p_qXPh{Gk!>yuidUaa7nmt0g?Py$oLEX?F;VIU{YYh-3%Y;0g) zYGiI`7zO5<8Cih222lnAhI|G*AY+(?Su6CC^K%U%4TRa)!FDn+LM>-zWM_6_U^#tr zzQMM|{Y~vx777*BIah4vTc?n8XU5GLi{~D`Fz1kb-PAOTMP07~^OFQr-sT^;(P5=_ zo-^|48NTpGTddU|DJ^bdGBRjl(lg)#S}4oU$oQXyg_()H0catRugby$@~t)-BP%OA zGb7klvce!mOa=@FvLGdVEMhDoYgmG59 z^z#ZY`leTRF(AiS!Xh@ZPq}E~DFY*z$&8FFY6dC>N-(|wQ=3>uNlAf~zJ98OUZ!47 zDli!J@{7{-4Gchr3bJS#s5h}|>48J1Ni;bnPp`N#FC{ZMu_QA;4=M}|JFrI-KprvR zU}H;fAYTj(Vh44t1n-1ol@zLQ+q^DV{_4%O{ zOZQxvwe#DzdD~16F1tG+_vGfHu$sM)G_4z3y^eP5w}uoPtZlz1`Y-WRtA>Gw&_3zxEr`KDMWl?o&3@D>HUrMU*x8|XXO!J zwex|-(uL(m9HXroH%k2k+JB|tXxKJ#hJ!v06IZ&bLLI!>wB&{dQZ>-X@Zn@h^`Nx8ayT(bqw NzbqABa@_mER{+coGKv5I literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/0dab0ae8be64366a.der b/tests/fixtures/diff_corpus/0dab0ae8be64366a.der new file mode 100644 index 0000000000000000000000000000000000000000..3e7b1618556843f36a09cc458cf9823124cbce6c GIT binary patch literal 964 zcmXqLVm@Hd#GJN(nTe5!NubPc*0Ti3i6VSWk3T-CKK#Xii;Y98&EuRc3p0~}wIR0w zCmVAp3!5-gXt1H2feeVlB`oTmpP!zSst{6CT3n(KoLW?tnVec|$OF_2lxG%Z4tF&) zkQ3)MGBYqXGBz|aG%_%Y0&~rb3=M$-1}0GkvWC(Ik`VPm<>lpi$vOF@DQP*0MX7qp z`MCy>2EuIYV3#m4LY>6S$j@Sb_xcKJ6d8|(hW(9LT6F96XFL?Xvug zjQ?4Ho@H+US_tH;vVgq7q0Pp~%F52nXuty!6b30`GGH)}1@ZV;#8^b`Fg)KHk=1=P zuqG<}^2LX{Qv$T_8OVdAm02VV#2Q2esrmy|(+@4|1;Z*+egVohn74Q|H?c?|7$%;JJz$TyZwG%9+Lv&wbgMf2|Rvl zHy^z;tJNne+?!*;N2@14cfaXBp28K%0JJwn@JP-Qp*PDrWfn2%G3;6Gufcvr&QyK% ztAAISPu8CO0JT>TZm&YlMX`jAh7;e}P96IH{?5_=Cw84TZ=UKOJ5#=RX`tjEf1u00 z6zKWQU2`zsF+aj?ae7I!Yq2hC_R$|QS2nMT>HOglf}EY0Js7~gxqN=<%(Uxovwd?I zWx4d40>nck^Im=LE^#}3eC{hn)m2Oi!6kYLV!}_(_hn!I$mQ*m>~mUUvzSUc_hhes K3BfYk4*&q;-aU{2 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/0e82620b9e99e2d6.der b/tests/fixtures/diff_corpus/0e82620b9e99e2d6.der new file mode 100644 index 0000000000000000000000000000000000000000..d7d167dff8410f3e0eebf3452d26ff60f569a45d GIT binary patch literal 1008 zcmXqLVt!-L#5`#MGZP~dlYrFIPT}r{j9#nMx#v%=?)YuM#m1r4=5fxJg_+49!I0a4 zlZ`o)g-w{r-N{hGKn%p;66Oz1O)kky&sXrt%*`xGO))e#Fa^ml3+usT0!ou|GLsd8 zQ;W({ixeD7OEOaPN-~oZOEUBG6r3FuT*4Gwjm-??#CeU(42%uT48R}?%moS<8Jk*~ zT1FX&8;TkTgN^3l(b7xLEXgQM(o4?IHHZWXvx6PV#0YgUGb1~*69Y^5M7eiIxT_b; zd1sK$dv49fEBAgR&G>eHMIzh(;@6(mGNoIRpXVms@|3J^=vda^zT&E==%XaXZ5J4} zOxf{Swp@8}6H}={6H}pqJkVZQWfq{X8$`rsu3$Ns|3tT71($#=^W~RPJJga5WI+n} zSj1RF@>)t?rG%_tlM>s)F8fKs`q=fQD-8HR()^5!|5<=x$=+bV1L6yV_)G>2U|-3q zvhaWcT$_!Nm6e^D(ZCa=M1jT0z`?+TjT@BM&1Jqd{W}Og$qbONBw1fj^9Iz|`gp(ht<0TvVcmYy>p+ zAdv@&JJhIic612?#+x2cBf>!jjBQ{C0S(MgE-pYe(V&TunT-qVk!zJqEKDqoUsxJH zGQ4LfW#9{+vgh-UEtB!tt&F>vU8G~4}?&=RKRr&cpSS@w^e`5a$|5r7XJDRVr|X>u(j?6u1tyy2US+Mvwznud^KG=bH287+Vp@4JI+-5du>{Azxw5h zGEXK2JI&Sqjr%`t5&NY1n@!-A-R7zw+dgIXZ_VoF+6Pjk4+Fyr7_ovZG6qsjDCxL~ z3n@(_Wj$sO2C&tSlS_~LUG;2w_KaQCeceibsi$>HvxTPIRF%IZuWd7llS#ox;7t?5 h!_SR|fqg5_f4Nru>DTR5S}X1;?ocqBoqtln5&$VnL_h!l literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/131fce7784016899.der b/tests/fixtures/diff_corpus/131fce7784016899.der new file mode 100644 index 0000000000000000000000000000000000000000..f583b217ae5d960d2ba534fbbbf2a5b1e492777f GIT binary patch literal 1290 zcmXqLVr4UEVtTiLnTe5!Ns!@?kdb%6$}JNM?>Kya)T(AH4p|VWftb~3=VQv2+GedQHU@!kQ3)MGBGeVG&TT(C?MAy$Tc!DHnlXhi~=%I zEEF>kfmq1vlUkx)tl*lLTvS<5V#s5_1u>mD$k523iBSpJ;f$;d%uS5^3_x)%rY1&4 zhFdc>fbJBHmWn5{^o^Y|5D*z?wNXWXBh)+eV%IMafTYM zTB;OV^hKBbkgtQyeMaS-73N>1QjJWWwH3}~dEdejy_ZR9=IdpQ46}+Sk@vO+nW;hMS1&4yu7Vd9MhSY zI`7|Gn`Ni{@}JE6$>6s6zof5L^5(v@V-l?e#{*LHtxKMIH1l&r#Of-~ja&Th#chU# zm%cof^!@a%>YMt!r)THrF8WkIM||yjVROriTTbXWUTZt>@{Qt;Na^~WRyFfu80RLj z9Mag!wbtg8z#C~MW=00a#f?7<8owIw0mDs}pONuD3kxs}v>C{P_^K?RfacI^ErSlqzTaY8EM+soN`21YRR85vpB3{(u1V0;6nHnEJ7k^(Dz z{R%_9Oud{`V07!{7p3ci6O*h03ozadIM~?I8+jO+7{Q5AR-Q${K+ZsBfz$#CWZlWg zx=~X#a}&@P{iPQ;&u5i+q#8}Sx7FoDYuOjkmL8`4Wv@4XeDpJ~WkuM;hm*X&KYYbd z;n80@>G2b{YUu=Rh4l;0i9B`X&3X{jw&e3I5s{TE<3HxQ{R|2acX;_kuJ)N|#J`o@ zf8Kwcm;4~@(cyAydB%3NDQ_JF?rfj==5F}qm_o;Q1_}%;ccyv=Xtd9q^lw$elx=Jr zB0prB>q;Dsazyd?$6Rcgd*tu!$b0W9^OSw3ewuV;hGtgGYo>!vAAWrGOHrs^@!0Ul zfecRf?i)KR7M$|)+Lx)Kc^z_(u#D2ceNd;TM z7WURHKR#I~uPJ13cwXP@X3~`9cm4ria{R8$S?-(yO8>k)wofb#IXzkKiP-Dl^VRpv zeD{Cb`1+aGOJkef%tbTyt*BqTI(}Qo3*Tug*L>a(by~X0Y_iJia}hxe^G&(pboZCc zPEr$}Gtp3!`*Z!-!%5v4$IHLebN@aT`IR}%ds)Ej3-6cyVcL@5%j})#e^i0J+hc>_ z*KKnyo?Kflac9$U{(TSJIPN|0zPEG7w&P;^%rfoUGQ3L~g*yX|W>qn*IqI>LuQoRE z$=Qo(3rdf(#=Yp=nk=cjJ7CL%H-}2i?MjzPH>+weDEb|^Y;}6hs{{Stxe2Ui hVtaRBTlf{;H9a@3FZkS^clgoimws0k*8DBA1^{mD_1pje literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/138bdf6e23ac971e.der b/tests/fixtures/diff_corpus/138bdf6e23ac971e.der new file mode 100644 index 0000000000000000000000000000000000000000..46289c19da4673c0f13859e5253e7824f0a2acab GIT binary patch literal 1122 zcmXqLVu>?oVs=}=%*4pVB+Onc4uMDj~a- zk(GhDiIJZHD9**y#K_2S_GPGC!&K&#kHvStUl?2Q^;4Av!~K&pjz{Tz|M7Zh+@!kt z{ficOHFHF~eEi0t@Itm~qw0n9{jHntuYKjxCmtl9b3oAZddTziqE{BDkI&57_A83v zBJ*3zuXokckF)GLxUNE1{bS>$c^Ok!Bi5=V>}Bk~z9jzR^;~lu7a=jVJJkzk+}O8Q zy)PFr%L`)tD&(Mxp`Cp9-A!6zpAilR(37x*ZkKXIc~M@%$1B}VrFDuT-?MM1q_ie10i5Y$_g_w{%7GZ zU;|Q2j0^^RATfTB7z;2tv>C{P_^K>o1|n=6+H8!htnAE;a269pnvX?{MMOSmQes$B zNPT144Ov^RRYo!GcR38?LAsS$Bn-qFL|P=yw3@ux!!u=y9!KPyg|F#Rzy*j;%SvVpC4XYNyZqueDIH?Wn(er1#pyOY_$c_K4Ti)rRD@5@bx zr}-aJ-<_!GoDx@bBKVD%DAP^;Je4wgPSMi*86F*pUw*IO@Z<+qXRlAx)5#wLq|^)x z_r36WyZ&Cy-O0+lVdYDkrY{rhI=#G?d9}y?b@LYNVV-BAa%bk1$u0Lxv$F1tXVPPoa5(NU)o=9z8c*#C+E+W52cZFWFnv3dMc40eXyjw=E|+} z&+UZl<~-s2pj@@YB0^{q?~V7u^LU!rWDO)!4PX05Yx#vz#l cJ6|<^mzXJf`@*AJcA0C6q7TkEc1`mN0BYTpH2?qr literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/15581c41023f0789.der b/tests/fixtures/diff_corpus/15581c41023f0789.der new file mode 100644 index 0000000000000000000000000000000000000000..d9d7c6144c3bca3611f9bc99c91ed7ed04cbdaa3 GIT binary patch literal 1666 zcmZ{kc~DbV6vp3u?-6zq1wkZWP(VNoHz5H8Wd{*Z5l|MZ5|hZDgd`9V6%uQ3zy+|# zCK`&ipcX`|ORFsEu((vMXhq773sMzZ1*%e;pjfAlf4rIdopbN+J9Fl|17(D5P^Pv~ zfuR6JNq}~mZXR(zd`Cr&X5l62pHP>eX)E`akLd!St%kq|nl{uVV5@L~KI-L5GloVg zp`}k+lE~k{N(_yQ;Zb<03q&j$722s%n7-vFDNk`COGF8Ap!lq&gaw6Cygey&NM}-3 zGN74}E}coE)14v1!2vo)DWwyn(HBlirzogTn+A!KL>&!5njntK7sfytnDc3cKn=lU zfQTsLTBr;F1 zm5pGz;kexj+kNFGEx)C^T{{}lyGJr-jOo)0+yo^9O?S>>u(SszCE<>+*s~i$Bh^ zLLjA5(NbQ!j z`)=pY3Zlbs)oThK43|R<0wG4K15g0H2wF-K4it1PfI$YNPw`M-Xca3K3q&sV_Nppl zCg)A0oHr_)pJXrO3izTpF<+Qw4|P;veN~w#M4LMOHp@On#8Qo{(1DXGP86|Os%cGTz<`egRPOeHAmXx9gxt>=Y_3rJ1rC)h6vNr7 za_8bRp<$-M3GahYFxov4W;Wf$w0Dek^$n#3`l~L?#yz1sT&!?aER;KaaAXS;Kezj` zRig5@*!H2nchgWD#btn}{Nh0@B0+-KzZX3?V0vuls9E;n?&`hAd@7vM*Oz{C=}q~8 zw@vj{2m(skuEC86f@6q3^h3>&om=|`^2Ri>iyV!w@cQOOWt4|;^n$rY$IFboOE#Z0 zS6&%WRB+I)N<-2l7f&A#o7(M9`TI}9yi&a zoz>MzC*2Er*m`n677}pzM&zOQ#w)WXjs2jHlG>OVkl!}2)ZM5=zdd!)#po8VC%=|6 z?9O}a$}G5OKG=%RwpkxT&9fId|!@cDdpf zzUeV9-Qtd8n#rL@+NbK<1J4*eC&oYU5R*w8(7}|UX2MW!+C4;&?8-dscic^ak$=`> zIA+Ydt$xy`!q~wh$vQ*GhnG?4n)sYLJ_kXT-GR-6oN+)2_As%kD-kA0@Vt zhQEfX!v3tIs+ft7;tFc0KEyqm_#&skM0@c}88&;E8k)%3RRWdY4MEI?`2R JWx23i^Is+3Uy=X- literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/179fbc148a3dd00f.der b/tests/fixtures/diff_corpus/179fbc148a3dd00f.der new file mode 100644 index 0000000000000000000000000000000000000000..fed8dab45f4189d92f2ea62a549206841bd10c3b GIT binary patch literal 546 zcmXqLVv;jxVqCI-nTe5!NiZSO^MRV)?$DoH8?faItY{wRxPgWnpGE2ryJJ zP-0^aW#JZGeqAijbzXF! zJ=KIuzUuC48{>sX=lU+mcbsZbe^RdH^~HIfliH`W^H{ZdrOlrkIoWr5B3ql+VkZL! z13qA=$nrBX{%2tU1~;1lKZq|3;kJQGfj`#uh>s1x_GJG%e!}} z`NI52lP)}vaXlW%KME}v#q?GC%S;CWoE zA9wSd=rYX(oN9_$NlXTXm$vrDys~~+!P&Z^bdvRnn7Ur)s&z|Lp SnIFGfKh!$?Q~Pt;1akl<_O(j@ literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/1af627c6c2ac992e.der b/tests/fixtures/diff_corpus/1af627c6c2ac992e.der new file mode 100644 index 0000000000000000000000000000000000000000..fe939f1115281b05275a44f58f1a7b24d7bbe6e9 GIT binary patch literal 982 zcmXqLV!mY1#9X<6nTe5!Ng!1rV&2EPtubjX#-HZvWq28Iv2kd%d7QIlVP-N2GUPVk zWMd9xVH0Kw4K|cC5C?I%cm$k%{6k&bd>n&Zbrd}Pob?Pf4AekU%sleWIr*h2X*r2S zsR|)J!3v(m#if~f=?bpS&I-^z}3KlGAjP^oomfK%US?1dc8+ zaCAL&T@Ce;Q;Q4~;aVBnM4?*qQM4MUf}A7BB4;4egd8qSyjpt5!K4gQrobX&AZ5V8 z#+Kg5!^p&Fz{kb|bSVpC(@FzIW=2L}NCFk9vhaW+8{!3KMgu#LpgfDYfvJJ<0>cIR za6crYI0U;NigJLK0QK{OLJyd9*c%M^**GDV-tyLAVPa)4Xkuhx;{pp^t7KwfVrl%! z()fwt149|ZiN#0ik8fVP(=?mWKC4P7_pz0q^U`B(Kc}y?ES)KPeij1*`>(k>Pax+oW_Jby7bb-=n;5}mUj<(7 z^<&J9`!kZ?J N`dfcDdNtow006;_H%R~h literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/1be011f72535b418.der b/tests/fixtures/diff_corpus/1be011f72535b418.der new file mode 100644 index 0000000000000000000000000000000000000000..168d7c8d73cb9eae7823e73304a2b48fec096486 GIT binary patch literal 1566 zcmXqLVv{mxVr5#u%*4pVB*3G|ZxsBDWB$Y-=BDiZa_<{ZqEMe$Kb08pfPzXG6rfB7 z3{P|#L~o=3&fd>=?vuzhYah&T_qR0zN@#mw`qdo>*I$Mqm=g7 zU){R4Y}QS_ptB4NjB^V#t3wzVSeO|+4BVI$89qHU{jsI${W_jYbJh0va2r=Ly4qy4 z_q886CUxrW(hYxsO19;me|OgIm~)6^tl($o?SF5rvMaZ-+tO?E^)nkw^71K%Pg9+vk|` z)d$m7YZUGRT06zhx&7ChgYO$ZORA`?kVs`bemUNPiP63&y*AWbPx1!T+H{1q8oghp z75tm4*p=W1P zRLp&C#GP@Al`F)z+Gq7ItJ-j{7hg)v*51$BcvmjB==JZf$9Xg^PrReI@519BwKso- zEu4Mk=fe~3spfH&M zd$#>*GwxNtmz(TvlN9Lwi1AM5!iC44=sM>5O8N9wOFarYXwN?{b;I<%6?Lbz+AX}) z?{56N!DeC3-mv%AZmHPZ6N|nT*Zf0I!@yh1BiZSE@zJs+>df_$>oSbD6^ZO@2=jfR jUVMLZ;Mex8({CAeP2X54QDO1_)%i>dmHKIKH~s+t04*XH literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/1dfc1605fbad358d.der b/tests/fixtures/diff_corpus/1dfc1605fbad358d.der new file mode 100644 index 0000000000000000000000000000000000000000..95399615ce617e226d37fd9e738a51e831776aff GIT binary patch literal 675 zcmXqLVw!Kz#H6}_nTe5!Nud6-WchR*C9g8kSmABgS|1y5v2kd%d7QIlVP-aPH{>?p zWMd9xVH0Kw4K`FVPylhbgeBec^V4%u6+((ii%S%OQ;W(nlT(Woe0-b@MGS;MYM6z& z-9v&Eg7WiA6oO0)EZ4^ip;GJhumX#N4WDl9E=ff zJtk@X{A$hHHt*SuH?Kat;GFAjQPU0MWw7 zBE}*zp?s2Q`jP)D&a8Y^eY2sm`po2?76$Sl6O~yc48$5l8hoz3F8S|iw)pCeKU?jN zW__3<+hbq?GoO)>MZ-YNKn2D(U}}@dC@Cqh($~+_E6C2&O9w`cevye@a#4waHpp0c z78L_v%q@^xAk!uVRho>X6rwl>7>Gc{3M{~|HQ-=lOK;?1WMV{44a^w~2B}Pj49}TX z2VVQ`JoQ}Zzl(xXuAXsltWw)|gXgo6uI+?%qWsDpLP7m@DV4(Ozb?~Z$_w4dWT4i0 zCWA5QdFSdn@fRZ3Tp5+}2=GPX>y$N#kefKB~0G*!1 A*#H0l literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/1f8eb9e9a8e066cc.der b/tests/fixtures/diff_corpus/1f8eb9e9a8e066cc.der new file mode 100644 index 0000000000000000000000000000000000000000..d751e5efd72dcb72325fdf0594c03b30900b1be6 GIT binary patch literal 1272 zcmXqLV)i_yj8i1v@GPdpH`Inkg6=7#S!yI~p1^F)AT@n30u% zxrvdV0VvMJ)WpchaOS%}lK-;Z$@y<51ce>VRj;lR3hVm#<>$5M5Bv8{UHvJ+QMBN; z5W_EZuC3t~(Y2cbCl^lT%W|9&Z7Th%`q%EG`&116g}p!Wxpw+vk*^}Yr4Tw?U}EM3wOtiAr^`RCO~evN;Cf;vpzz0&m z&&c?ng$0=S+92ksvWOXouyJU!F|x9MxHWR3-1Jg7kLuvN`o9Ord3u3anzAC<7WBAP3wqgDC(3NtB`y_HI zFMnU-bicpj=-e%(pHFi9?fMt^`O{nlDHhXnKejnmuK#~#THKV&vt$eO78!Q`UV5fJ z?7q19^!*2Wo!eITq}A}1GPgOHKRf&pda=%Cb=TG3MU^Sb9`l86w4Q3eHo-W(Yt6Rh kF(2MW1xa5?e5fk)jD72>DVsiMd`nb|3zhl7_`19h0L13S4FCWD literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/2585928d2c5bfd95.der b/tests/fixtures/diff_corpus/2585928d2c5bfd95.der new file mode 100644 index 0000000000000000000000000000000000000000..955063d86efc86225ab8ade021ca02c754b2b279 GIT binary patch literal 950 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/2a317f1f5d8267af.der b/tests/fixtures/diff_corpus/2a317f1f5d8267af.der new file mode 100644 index 0000000000000000000000000000000000000000..ba0eb061924bbad54ddd14632f528cab71ef6b9a GIT binary patch literal 1308 zcmXqLVwEsxVqsXo%*4pVBq0B8tB$>x-CnMeq^i#BYy$&cHcqWJkGAi;jEvl@3%&@>9nH8(IfG%zwXF^w`1G~@@`2UgFPp0AgZT4vD1sD$hU zMpg#qCPsb+pg0#(6C)$T6@}fkQSD35b^PW07a}6ezL4ej!Ii7^yH`zLc|vdBllL#D z#)wV(JA1!1J5#7w{Vh`mm)U6%7jFcHr}7&bMuZxPTyXlgA;L{{R!?yHgijt~LjQ7$ z%lsDbbe=W!6?xn``U}=)!Swqd9N{X!}eoGwKj3< zua;$Gv|PQyDJ!+c&ML=b<3aIx=6$Q}6XY#K|DSdF&2^fzO=KE({&F8v&{~9 z#__$nMvL9VKprHm%pzeR)*y1c@$28dj++cNzR8OM3{@7KQ~sxH5C=1vk&z|HAizK! z#y4PWlg=n9DX`Ml&(|x+&eTf>rV#yN{qiDXeGgp|168;}rZ!2a!c2rhpfbJWq7nl^ zkOKr+xC}U&U}*yED+Q3R3^>@>(i?dgnHUYsKB4<1gc{ zGwlD_bkaB~_2GrhjK$&yP8`iFXJBBQo3m2(69WSaGlRQc1`mWyM-lhqTua6&6j8fWPe|78HvROCzg3dzhmE!~3>tWyq zG1g#~OVR#$55zY)Fq{)}D>x_yRI)0wby?O5r9ZhIWjbLQO2%aUtyk4!!}yN1PX%^AD7oxd*CCih*d zj}3j=ZT8Xktla%{i(Sp_J=+CsriQ*!dbP6kw_Dhp{1*z+w|y5#X$9GBo-kqKf#;0o z8qcg|AK0fN6np85tibYu8>wHz{gvl46?pF8U6>W2P|ta#_J&DhhSE>3y7pzUzG|^& r$|I-PzN+cHJt0QM@6SW;rKU%w-?VhP_V?!!wshuK_6seHL$(0`NyX$| literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/2a575471e31340bc.der b/tests/fixtures/diff_corpus/2a575471e31340bc.der new file mode 100644 index 0000000000000000000000000000000000000000..c0310642c08c8930b7b3c7330bf12d77539e33ed GIT binary patch literal 1374 zcmXqLVvRCrVs=`<%*4pVB#`I6@~F2v5APe}3!OV!|u1Q{C0iSrto8JHOv8Gu2QIIl4>mwFa8F)1N?f{~ShxrvFN!JvtW zi>Zl;kzt!4Q@7mXh2^uY@9dvqw|(kz2b+a|n|{_Bu{9Nb`_@zc`^D`&S$fl_FYfXF zsr_x^&vPxZ8-E_1e((LxPm5pf5$cRvzA9+-4`ub!OE%6)zI}aW-3P9?*BQ&c9E)=2 z^%XFF?6*;P@d=3>?>B!qC)}T#qN%FPzHrMWTfKw_=Pz-#?>_7C!S~Ris*bMRyUs^S zUR&S4fqla~!;eh|58T&VmYXBVxAjJynv^TE?S{%FQ#97{*Q-5U!SQd}3!987!S9-_ zIZO-fEVJZF7{4>_sPCwMre_)Tp0q&T+-`Y2u=B$wjsBd}Tb9T~Y zg_p*Af@ZEa+N!x(RcnjK&!UH?QocWH66k#~_fS{IFN?IUKgmBCe^&ghjw%0D*8c0w z{f&H;Q^U9HReOJp@#1SSuHAjyTaO;?3SE79&8vytnr9@J7^(VceN1(^z;tfaInOPx z=GQcDaHxLnyn1=g;{Mvs#$P^5|6lDpe9rs9)*GAqT+f#scYK8i} zI8PCVMGW<`Rw!;;xpC@-r8{}W1aBSB@vw1tP{$i}Hq7SYbk?1xz291T&pBd$DO5O5 zWy7_?UtbRHI`dZg>%T_F8w@jStxl|~nqe-%I+cl;k%4islYxT)A26B9@-s62XJG+m z1~vnJ5MLO?XEk63QU0I87yo|l9B$+J+7C|sz0}7g?^v?5W>w3Tr=R4WnFkro;l7wCyInyCSyYJWA~ci6%)MF-+N!5Klyo+*|}L6JWbEfUl96{km6X*QOV6i@v)a6P9JX^s%P}&yL#Py{$_&M#g=~dU$u?w-lFk&jOnj z(~b$+eLm;@ZjOe$>aG84(x%=u<&Sx;;xlv{zT20+}Nx+-K!?5Xp7~A?d7K{ z|7KcEGJl(Ak?L0WGGT9J$Mkp$p)c>&M>m~lyZUfuZV$_+vR$6v@?Pf3#V>fT-c;uB zW7*}17=9h6TTaKSJ4}uryx}_gpxx0#*_+7$p5IJbvmQts&2wMv=%hLOTLSMT!^_Dt zlqW|qG|us?p{IzMhLx@Cxmo?H&l(C7wYOHE`Kc<__|PzZ z;o>^6C3D2Grc3NCD_Y7uPuA?2{zCnI1rzyKJiGPd$Rvjx)w=4fuWfMokg>FX zO6mWSs23}kYIrywt@P$z(8`^(^Zi1@Nx$EJ3uNicd9ku&;coHGTmO3U*Vw#imQZl^ zyE=K-7L$$gVrKF@&r&SKeC|B>CwpRLlC8t5Q%6mhwyf*yFgCrTBV2q#{o{qAccs=! Y_hw|IMSnb&>>et!Ybi%y=`s&h0Er}BD*ylh literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/2ac5352a4c603fff.der b/tests/fixtures/diff_corpus/2ac5352a4c603fff.der new file mode 100644 index 0000000000000000000000000000000000000000..76b175f6b24dce396addbc003d45f65b126d509f GIT binary patch literal 1480 zcmZvceN|1S;&D3!?I&xp@tHkG(=6*42|SeqMTqsX!lN}bxwcWKc46Qp5N#BexK)F zaB@z8lS9|z7>Z$m#p)}_x4i)wA^rma`^}B@;K%ieb!pG52^@n=${&2Wcs9WmV%b@g z5Q5miPw2PAYBd?jbhAN5#lbu_i3=kiCs%LIH_-+%HHXYH+DeT!GF77?HA_f}j3bv( zFlR=9JQ3vbL~0QzsKoW2FhQ=Mk|1ee(t4=upu!-Oy-~myt+vsndb^QaWh>H~=?cA_ zwwOI#_|M~_1nN|%FckN+77DYtUP+Aar_r0}LW|8z>!~0RuorTKJ{njAi3v&a|JCL5 zpQ2z0WQ%xwU(IH#(PpR3mMH`H-Udt_#QDB@))kq-@&f2U$mMx##L!7Uag>LrAjIh* zo+9RrGpRpbzdfGb)^eiP=iZv^#eXzxtu`tC-nUh?S9M}=X17Xpx2|_EcO5N{X#v{So9c=(6FS#=w_ zo-d26akqrFKn2%_@r=Xap(tSn6Ud*$fVt)t+wE3me7u2?St6TK5wEuzOG}If164%H z3XC>LoT@=xv6D3xLy6U^gbiUqoL41KAYz*fA_BnQ>E%%#nQP+eCLzx*cgeZc~ z?(}2vI0^}DGMtdXe2A@=*3WfCK2dKoc|p7s$P6|Ugt2AQ2r(g&G+v%I7X9nKXRj67 z`;|vCFG&NcVH*2dz)pktZg|_7h4W>`?;14}aB~QZgMbqq<@$NHwznK3Fv5urFi$^v z3n@k3tSxBIURT>Zs@YYn@JVsp9a3HzX&6p4r!^!C&NZGv5cJ^N^Yr-$Lg2_UNW~(M z9(V3|5mHhqzQ}74th{}5clR79vz*^HQnjgW z?(wkomFN2W-5ovKbuS_gq#)>&sUi5c52|Osi^-4^O{=85o;eB8O@j|F;b*sW4^48z zJzOoMG2F9toE`T9n>S)~nU<>kM6Xyb$lpt+Z2EmZr|;Z%2oV$Q9td9` z{Bvv)teFx_MztmDHmqGepkFw@ea-6<^I?u8D>tQ8{>9kKkv+X&WVpk=YwwpywH4+& z88yLaGeRxg;dk=)L>$szo# zU7`-rx`9cq@?UPUV7xNd2mG%622 zc~X2lx*usbou)>r9E)+SZ{P@j(BW5)^*-}WBMDH&u?g#6vS()Qxj~z56)yZ#_0>OR-rP(8 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/2af988f26f6ef0da.der b/tests/fixtures/diff_corpus/2af988f26f6ef0da.der new file mode 100644 index 0000000000000000000000000000000000000000..310ee6e84cca1b488255a7f89f2b539bd3beeb70 GIT binary patch literal 817 zcmXqLV%9ZiV%ofbnTe5!Nq}uji2Y~ljWg5_wl7e$J1Awq#m1r4=5fxJg_+qP(U9AK zlZ`o)g-w_#G}utoKp4c~66SHqOwV*qEh~a|;WUmzV3M0HurSUSk6w#}LXj@I!Ti zuz?`N30#f^1v#l8`}GX93^X9NC_)4SN|SOjlNEwfi^@`q6kMI16`UOv3>9=0+zkyP zfqq~Idyt6{>QiP$c4j9AmV`A7zgRbHYv;DhiJBg9>P`OCMSr&XZN0hYvdI1=yHrlz zJsV)2OC>@BM&gi8CbA! zCA4`kw*AdvU}7-P u7ZQ4S%bMehLr!V8G7G)7Ro{N0I?3;TS_lu%)IEECZ`v^PZlayAZ8!iz9r9=Z literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/2ce1cb0bf9d2f9e1.der b/tests/fixtures/diff_corpus/2ce1cb0bf9d2f9e1.der new file mode 100644 index 0000000000000000000000000000000000000000..cf6588c801db52a03751774b92da032ff0c00fa8 GIT binary patch literal 993 zcmXqLV!mt8#B_84GZP~d6C;BGFB_*;n@8JsUPeZ4RtAH{enV~pPB!LH7B*p~&|pIW z13nOkLzvyMD6=X*FVRrgKoBItF3c62oL^E>oRXN6YN%?U3=-rLmI*FNEK18v%}G%R zNlnhk%g@PA&rB`WQSi)5)-yCRFaRlI7S=>43(C(gQE*NzD#=XCOinCGRd6gV$;dCt zEU8q`Rd6>lkQ3)MumnOw12BjZ=QTDrG%^MvQ%h6Js6puBCPpRXU}a=wU~XdMXE11D z-d`>a1oOIxgTU`Ca++d%i^=IX| zlXqLq{r#tsVMb!fd9@~oBcjv*#k4g3Mo8warek|YaTNWi( z7H4wc#k8=QU7J7DFFCvSj*(%_#Pbj01%G6(@{GASbIQd{6>DlMF}~T+m0oT5 zC|$2-DzloN^IR{(Z+u&Mc5i=VUaz!^VKHBI(C_Ts)<-T^2Kt?y@FksH@`R0B6)$7$ z(TL}{&sq({XQ> z^=Zx&S8i*E#_zQ?Mqg)~is-A=dN(1mIOV5VNu27!uW77$^_HS5wSEV5_qAqz0RZW3 Bb(jDE literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/2d74167e675e0223.der b/tests/fixtures/diff_corpus/2d74167e675e0223.der new file mode 100644 index 0000000000000000000000000000000000000000..df4c4d9a2ad7d4dfc51f029001c2f75c60df0214 GIT binary patch literal 1497 zcmXqLV!dk6#Ikn*GZP~dlK@|nh&j1wXVrpV!WVo{C)pH}q=D&~M=(mX5dz1y)JM6r=wW&TP*xt=QhcEns&DqkM zu6J!XyS-mcvc7HLDWJ~2I>dj{#$M|$1zSxWZ@gjP2;Z<;`Q94oZI~o|e*VYVF|Xg8 zSZcgVP~nyT`L}JS{+|hof0VuG=ZoC$rf*Y9x37O_mFyhx+DRx;S=ll$>tykrh*{42 z_4S&pO6nE5gfDSCJ$CkDTA`r*8U?lK@BFQ2*Vskf{2aR5;YN%Ao6u6v;|Ggo>mK>g z_hXyEHKDTthj~3VDc$^}>i)BD#*L#)%!~|-i<_8!8ZyvbJ2SjAGjFau~>h6!5W#v4~9hn$ETNn`gFWuKqQD?TZCrahF65j6u?Z zEII~SO^7&X;!DcOPuGLTNfU2*c{ze>APmx^z`|p|Wx&D4mfpz2$i!&C2NL9GWc<&< z!py|F0HR10q==0}n~jl`m7STGRieD z(Mtp;t&*bB;u27L(1(ZuYtebp6XQ3892V3lJ z;L4;RrR}=RBO zaJV0!TF_(6n^sxL{h^Ch@uAvbpogdUIk*3MbMSrRXGs;c6%wh8$1lfQFfrN}rPqd< z>q*{#*qhCa+E*QhZEDj6-cFl$y1zLQvt|skH z{Li2J4eqnsKIHCKZI`=Mp^_Nw{9D2?a8sWza`6BxrGUi)BZJbrfR~x`Wt%=GyzI-# zi95)#`jh>|(tE4y98WJ!ElN)B4*lSBa{ji89M`gaw(IO8FK3>8q0iiWaK-0KiAUE? zFMjw%`G$hVj^kYt7VM8soGbVDeY4|N!nE-Hy^&UCA(rQw4*I<0TElU6PZ4)mz`eV* z-O;~=eZ>qH-bm=(e853pG&E8=Z!!Cl%jvb9Z3h#M|CQ69$#;5L-tLLZTX?nFe;4sg zYJGO)*wf6vmxS4`wFWCk)y$mOnWL=rTWxE5Rm=7tGxgr(p3z~>3z)Fy)7DjOb3>i( zKcAXs`pQVThJWGYDL1S6OSvMxNZrrNuFHSwp`$8xQg-JZjoAjd+mpQt92o@x^^+<0 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/2f843e8a48ae698e.der b/tests/fixtures/diff_corpus/2f843e8a48ae698e.der new file mode 100644 index 0000000000000000000000000000000000000000..3b5077c3852057a64d80dd0aade819a25fd73e60 GIT binary patch literal 937 zcmXqLVqR*{#O%F*nTe5!NkH}R^w;)(k5~!MkT0k)7sl`Z%u6L?_HhQSceR(TAyDr6l&{ zd|hf|pK<-?H0DK?pPEwM8m9_=o^zo`d#U(tKa+2Zo0vijnwSC%_<+{R@-s62X94<> zy#Z(;kgv)DazBSQ8zU<#J2RsJ4@giLq=?CY!9W(o<6{wH5n&DHT@b$Q{)f%4&m8vp z(34qv?q-dFJV;uZMZ!R=L1aStB-8XG|5uz@`KJ2~s#I=X2@rz#6E86YqEyBRSIJZ}FW;O!@3p0befh&`OK7WqzGl#c_ z(q^7FeA3D_ztqz5pGcm~MxW$mMg~RVDolzD`&77>F7(^O(eyGu>i6`{yMDa}fs@Ye z;CS--((xFF>`sPqhADo|?Z4g}eBbz4QblcrL@ML)%kdUWjP^z8wV~#Ek~g6CI)Ux= zFmMB!+Innz5aZ3DxLq%vcd;;7ZEOtLbgm+D`ON&eyEb31Q%wOX`Nfiv6{w?Ezb__c z(u2ypR?CpjGv8Hjop_q%ccWs8hCXthVfJ7Ec}M&Fv>!)Y7yl5gJ?Fk9`ZTl8))OJ5mdMjG<9Mn+ndJr%ev7w|0-oCwHWChVbM9z*phN;+M&NHXWv~10M|w@ A0RR91 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/2fe357db13751ff9.der b/tests/fixtures/diff_corpus/2fe357db13751ff9.der new file mode 100644 index 0000000000000000000000000000000000000000..6d1115b95412562aa7791781395a55ffebb6d1d9 GIT binary patch literal 1295 zcmXqLV&yhyV*0#*nTe5!Nud4%>q^m#YgT)d+QeE`c}E%WvTx zn1#9BLxL58^7Bg+f(#Ah#CeU34UG(q4J-|SAPUH}G%zwUFf;*k4XjZuk~5HjSR{(o zB10YnE{G+};X%d*O^iy&4rgR#U~XdMX8?+GF*PwVGW462wy$p~66s|Zn3&JJ=HnF( z(^-x+BRH9a9hyDGbWOYrj6%=x_6;>n7*W5%c5I`iEYyfjbl zy>sU3bU&lD)w0U0JHJ+YI?uSUC;WJDg~Z0U%R@HRwC(#4;<~4O*UU)SFFS5C{BHDj z(@u)pXth*JTzSo+eMkGA#{Dgnt6j21xt1kplgXv~GuN~q^NEcA@yx%kbkd6KhxAunE?Zx$x{t`7>v) zL)(9NEIPY3d8=ynv?AF?*Zj83BTt@H_!vH0SihXHn5+GLduP#^%SU^em>C%u7dQSh zX#8!!2Mjk^en!UsEG)nz&}JYD;;XWV8HliPXtOc0va&NX!dXn zAY?iD-|b{_`j2UGB^dUXxe%EX%E%#hG`-MF*oZ zRz}Qod*h9QqxQPp2u=9!VB31@!vE#>zT|#=z30@CThoPhTNzD!yTwaSa#z~T-^J_F z_18T!^!*d_Ah+#+#CDya7iVr2UG81L`}N=%o8{L|M@604{gd;vQAp*Nm|s1@OP=-0ba*+(}E4nyk)-odwUA|hlb&px&-v!X zt1k5|T+YirC0;Xf#)oCQzRx;b&kR+S87MeA8XCxn^BS2Mm>5_XfI$?PYi?w0WNd0_Y8hpai)yv7 zfgs2Z4q-0m#GK5u{GziJ zRpFX5BS&J(wS;vUSMJWro}s&cZQjGDO!&A7OUd8$DZ^F#xAplf86StJa^8brF3U(d_F{?k16XRe}U*{p@>_XJiL$buB` zv52vVB+s{T`KqB08mO~zXtOc0va&NVib-dbloVL$>z9|8>!kqWF&P+-pfJ#P4hS~j1L@~y zWc<&+&DIvzkQrcU)X;xM@xVLpj6s+**N)%fFT%I{q^LI>Y{- zO(%_`QXgK}%vdab;Kb3)a;UvAV0%3b+<^ABb=7kps*QVpWl3q{0yTjTf8X#`zPX}0 zqbYyZ60a>0jX)(I`9Izh;ETAlu)zOs@4AC?&L1tklCf}qm-V`a2{8=Y9DyF5;^*A{ z>&?OUjh`h|)K*BOG9JGiZ^6W9UzA=OYOW`F18VV9Bo7}w#1Z+`YH2=uR`)Lh+x)VM z`Ti9%^5V@|MO)LB7IHH)Dfk=|otG9NmYH$rYVDB~YvY?9M)sb*_)2Z|6v3r2Ns=>= zOFHIs27?qP1LL=T^Ax*Odu}g#&Bz?G^ZvXDL$7?S*YoFo&d5X*`C-$BN4yR>* z%${Mo(Bhl~lR;92ae+jA`FpRD1g?66{0EyRvd6ScskNJUqv5)+$mxCVQVAY4CZ^Xi M)m}Wgoz~e702$84MF0Q* literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/31ad6648f8104138.der b/tests/fixtures/diff_corpus/31ad6648f8104138.der new file mode 100644 index 0000000000000000000000000000000000000000..6dda6a38dc3d4d361acd02e4b52d4759d26ca563 GIT binary patch literal 579 zcmXqLVzM`AVm!KlnTe5!Nq{vpY|p2-C8nl_m+)U*?OJ5O#m1r4=5fxJg_+qP(U9AK zlZ`o)g-w_#G}utoKp4c~66SHqOwV*qEh~a|;WUmzV3M0HuD260TGTzWX6 z%s_yR9qeZ&MmARMMivGo<|GD|yUTCx?%Kiqo4a1|(uaNfia#62O6@Uub??~hJr|~a z`*7w0_pZd~K)1Siy7S-lCG&{CVK4Z4zD3WWdCm)a$6V{RmzRh{npMnYc77)yoKtjf zUB~i;*;}2@rQcEh&n&a}k=^gdpVegNSN3)(r*KAf72LSq%!wCQY literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/340ca5ba402d140b.der b/tests/fixtures/diff_corpus/340ca5ba402d140b.der new file mode 100644 index 0000000000000000000000000000000000000000..ffaade18134eacd6b0b8f506ebc8c10aed9eea50 GIT binary patch literal 1314 zcmXqLVwE#!Vqsgr%*4pVB*5}X?~LM~Ef!jby8CrgBubpu7|1m= zFful^G_{N}2t;*)uz?`N30#f^1v#l8`}GWU4YVP)s6Ye*N|SOjlNDUU6oONW%2JCI zf`T0toE;Sm6?7Hc4Go$Ym5@Ek$jZRn#K_M86z5`UVq|2vKY#k&a0iP5W!K$~`a$e@ zsllIYXRJNlwa4{saDji8wCakmImc&TUb@3F>)`2krJFvmzTIh?V3vNzb!kx7H^XT! zqpQE%dsM$YzWa{Z@0?@5|8VXOtMcI&6u5GA$CO*fTodAXWBuG*tT+w{EZhC;&E~lH zWr}Nc9;6peP>7k5WE){EA{O-c4&OYMo_l_slc#Tbs2IQ8(NWBArGfP1J4a6{?ce(R zTJ~@Get7#j&FbQJ>$LC(#v0G>G(50ahw==?8yzjT*=pyr@9}q ze4D3T`}yvY2#{VoVz(m&uF;A66%s_;VLz|6}m6e^D5zb;V z5CSO|1}Wz-U;|Q2j0^@QFnLBs77YV60~HwGfU!*?qokz3N?$)exwrr&?HM?MOp<4@ zHn21>UtqexxXlo%G`T3p7)7Z*EOA28CMapT8|eX+88qGj8KS__c*UUcqCw+PHqL}L z55~5?IZTWOjoS@0U=Cqo6q5m&UJP^zdM40!4hS}A?1C$0GN|LJam=e!C@oG^$WK!! z$;d2L0A;4kw9Mqhl2nD{{Ji3l%#zZQ)MACi znswPw%A}z9gK5r>o?m;j`sYOR8!mr!ZrQ62h1pl-ST;-wxRl$n{JzPrRfn!}S3Z?3 zJ1uu`@yz(w?LGO?$)ESChX!oP4p+`dFIV92iqbMO|JfnB^_A>RMaKaB+S-CU{9xqZL<`uZs!#Bbj-lDft=EkJgEa@;Xdo;u^^7PZ9-jnXET zeNvic>~m9rxB9}eEBA_azrM8jv%t~$eitV#n|6--wf!^0?HTjh)aq_%2$hRzz^>iz=R3LI z1*S0sFga!Hq9HJXc+kt6Ta{S_F=)*mjP-WD9=I}``Q~JEuPY3@&>^r?=mMM>G#ScZ zBeFE17hqEclg*09R9Ku+NAPBA6hbi&Qf?ZXKjKbO<16H9UXqBnXl@!WH7$mhmK-CJ zMDRpF1bA^VLNL+WRTKflLMa&hrBkL50Fg*6nI@T*4&E6J%?4vBj4^EH61jdArcn{N zM(6?DD5n$KDOI^L8`rE9P6QK%I`o={DRj6V&mwqnxK4}fdb@P1**MPGh&nVv`*VW^dYvWt)=}B z?qN9#BWj{gZ#pl1(sBGG2rUtTnp)}mlb3IAdm}Y z+kyV`794!Tk6&wX@4qqoUSKz;DLn zk^Dt@#UJN$ygsa}>6LFx2$0mQZElS|QPuSb4Tj(^O!Q!2q6Ywfl0j}D>H|21p2Wt&vso8z zM|QT&o%Gp@5}5Dl07`V+k(?EyvWCGZ-o(iVEnq*hSN2#;HsHh5{d?l3%nqBMlQYfx0U!6*wb?GXuEbsO=Gft?3Y}U!x~yGzhIA)MM5+!cIzyU6Ne-zvVL}0!_xDOu|8FH@ zk-c3QM%RK|5M;LjZ1x#$6<6rBqX590BTiLeqY(@39kUi4}KG}iPnz!_&xa=Ot?Lh*d z&2p#834s~xsA{Qym2X_58(zgohd!8K#hT_sJ)HkJlmoRp%c3h5f)c|FWnX{Zr>)| z+P=NaXB)!Pmgmj41jpMR%(z>mfaYcQ$#$Sy+ zzCn||^ijeWS~H~xJWHoqCyNyLwhzO8G%{`gRPV}Z)LpB*(UlkB;{5br%GPqjn!I23 z-nWWN=FJ#eI0oV6CQf-f6+a$-_;Pu2@twmRyNeIq{hHZOM~~#+SU&!|tK(BbKBDAP z0z(&XR{6At)tB9Fzh3&|8}n-OrNI`x&3oYsY^l1pcTyR<_oV4J!g_UftGFdoQy3(< zh^r^PW%#~iaRf2ZmbWPS+VQ6wbZz6kC#uq#B@LWY4epPvG{MD;b)|(*C)A}nr)Dm( zm3mmuRd;O$&aH`4zmd+!{ayaZk`rotTKBr*QPcH=qEe}b_KGGA@ z|0E>S@2oBQRtAVDs+c_9IMA{CluMDhs-lpiX^F)pIhA_J z`MCy7j7rFkVq|4tZerwT0E%-lH8C&^{3*vq{SCEm}N`+HNVq!m|r>$<(?uc|R! zJ~D-gr-nISprvy5DXxReGZwpa?lNH9{BF;h?QMIfbCz<)ot}7r!MxeteD}JzwF_=b zhwJ<0&ARWqDaQJ-L)oPFydHbgCo#Q|n7YR!XTd8+nWQZ~=4A^$JlR%Y{XyU4U9a-J zBey0jk$07pm2k4J6M113U%?x?d4KzjW-pgb@w`)~oEGp(Og29hmRb&dg}Q0}>PlDPl5UFpvfD_*lePM0|zU zMQ#vTP}BXju=KUN%zIsq+NTEcAZcY52?MbPk-gLB-d}h_T6N}Q9u~IF6rt;>D#s1X zVJ0&&vS=G<8mPng225?z86_nJR{Hvxc_q3S@1}Vb(l@HUF6C`kEs0Kiw8Nt*K-q4ReBY(b=XgH>m0XZfjxvz*&OQC*(t?Fb z3zH_KO+9t-)z?>g5Bnlksn0tNwD@A)%B30Xb!nyx?9G?n-NU@p#!*P*N{7VPmxh~_ zPfulmSZulr$>MEWZaobB^?R-443Nw ziPaOUooBoyR$Z^I3;y8KDBU}C!BgLtSx zDcqs4$}+U_eWwdsOj@aG(#i?1=7+oGtxTxnnEqDvXwRB&zqXpp_*@|MryzMmHpgg%_8MllgifnF!pxGfrWaPf zy{9~B`8(0QyEYe^{{Fphb<~Bre{3JQR(siO*;UgdmUP!`p&LW{e*vCa0ZsE-xHp33K^ZfQ*&@b&M#!=@6{Sup!medC#u$&81~XWXJZ_pslvh-= zj8;kBmD)NT%A=z6Fey67?n*?h*p8BAXOgzl$vx+ubHDHR{eQpz_qZQGaa90S%9COc z7(tdQmz7!#ixSOZs-;CqCfR;K9j9t0-YlL{hha4(2skj*fGQ5-iNz5RPY;Hs9G4q- z0?vugiWBT%7=Yyp2ZlhHcTn6toG47NW-VnKU;;y3b*442W`g<0M{e2x8^&6um9?F< zoewZ#kO67_JdrKfBNXsBbXJUz&iX)3XAAfMg^fSI6RZSbP*@X^`|$`0!%$a5UBRZo zL>$r`^)UDPwUUWj(9+;@+eagO&6^QcZ%k_0S$dQ6+Wf0JEAkqeP7Ypvy=v}Efc?xt z+TQL&m!aLnjbSNU{44dQZD*QHCSKS)E^@D@in*9Ud=*iaUZ~pWbFTs3aNRevVd56x*9?I|_Gict0E`S;n?Qy~Wjzz9jRD z=1JwQy>xyEqs@Y&IfCWs>L-X1yeE^ZY4^Fy^s8dxG$r^bSMugb-(#J@mq zck!`Y!wm9_2}qez5NKgj{st*y=psEqDUlqZAS#m05z1TiwXUILGkQ!&AeSRrEI?mq zwHQR=@&o}a-eRzU*#ct03J>{WTu?n;2sc^@E0&3C=#b&mjFCa z!Qr?mOJD>B8VaW-49{V)7(_`rUp*R@2y}2Nt8uWBk}?MV^o0O>MK~F|Mn3sKN|&08 z&4NXu@NX?G*^xYYB$E%AA17NB86{MdhHp9VmE$04fe41NA%F`k7Hj~EjXv@S*uwCy z69Jn~;mBvQqOV6U$Z{gV62+YfiWCu`_^T3dczI@6_)#o^MX)H$lfS`m5EKn*cptes zSzYdEwZi+|~io9-Vu+3U@qBSuh;7}_p5)fF~Utds9iY4m+NL=1BA3M-v- z$f$T7YT5=xL4jH=yi)D4vNVH}2pzgwv3-^DLqqG86)$GHFl~D~#y{%Le9(0P8xab> zg5wiE%2+Rv&AvAvX4cmAaN6BguS`AIR>HWcx^e=ccplRptoJEO(@%coKiZh$lsR!| zuDqig-=llCl}mrOOfKtsqA;YNlrT19)8BC*@bvFL)!+{#>p$>xe?Ky6+V{43UbY4Q zzpTcME4!Y|t*gV+hAr_WMX&ySs`$qcx z*rSEflmzo#lG&0=1wjM*XFlI_^6e|%bTD)(P_tG!O~uX6AARY|9bB>RPExx?wZp0B z8C#4K)rQnKImE)^t+K$MH%s5n(hAf^@?F%YaI4!5c^2_6@U^ur4!7(%-yAA8zBDyn z&(8UAKmV|1Uy|j^u2}nO07b-+hol@&8LeQpm-43_Zoa;0Y3SCtM$o1*mF!YG4)6<$p*TR9J z$;@*N+78|{B{<()Ems_39er;o=>q#3pLD(slg!R}>{v#P6NYee<$-0Qv b0rc6kP-!;=)_IkfhEh|X6UQeS2b2E;oS==T literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/3fcb4dc9da35fe86.der b/tests/fixtures/diff_corpus/3fcb4dc9da35fe86.der new file mode 100644 index 0000000000000000000000000000000000000000..6f16bad3cd22882ff129e25766d910d6692f9f3b GIT binary patch literal 1173 zcmXqLVwq^r#9X<6nTe5!Nucz|ch9o3U%dpjHk2Kx>6S9!W#iOp^Jx3d%gD&h%3xq^ z$Zf#M#vIDRCd?EXY$#_S1LANAi@N9Mr{|<9gcOw)mna0M7L{ctrxqLX7;u5)nT46d zgNzL1#CeU(42+G84J?c;O--V}Tr)#cFxNQBK+aIcKnkK>I59anzcjC;ST7xHuwHV0 zu0f=MFdIA2HRo)Z7}+?$3}!}lW+w)gKG~_2v%<^v3)xO;l1b{8e15uZlAYzNPd*=W z)^F+RyZ`xrfj!S%!5cR@M0kVRnpS_|X1UgJ!OU3L(($w5L6;4So0!TCnwW|W_<+{S z@-s62XJKJxVs8Li2;{4>fc(Ls&Bn;e%FfJazylH#1}S1PU@(vc@%UK8SVVRh&)Yt) zJ)|#HgGboyqk8vRMw8_9A0%kHJ zBa4rLmw_UTZ@}0lno&|xV5P60uUC+r2@VSV@**PxRk#wSHc6p3T7G?%_16L+RhA(AXwYKL?@4wm{T@q1{={lvT;LOyYxr@HYdx}5#_TfL1 zf~EfkUpc3@)hAzFc3JgfReO!hD&2QN+W+Dj(u9AN9NPi3_q4@#(axHdg)Z^Y2k-tq znfGZ{yiM$K)-D{>t4x!~R<|al)1~r|` zPj40;*4lHR^y+h~zBy)(3(PP4wqDE8u;wXqcJ*ksL|L^@ymv38sozeG{veS=RdFiA4dKbozhq|xszj5Y3x}sIa z4CU9X(-!A+JeMhm_!TD|B3Y#RKq2^Dz9)P6-G@`oq;{3==ZKGd`+I8BwK(b5%5rc^M4yEs zEt216`0F(4N|;@htV)_M=vlV->6wG68O?k1-2wwTFB@{SCwM;Do96QI#?F6p0&U%r L1=h3}%{2r7uqvHL literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/4026117dc787e06a.der b/tests/fixtures/diff_corpus/4026117dc787e06a.der new file mode 100644 index 0000000000000000000000000000000000000000..1c53f5d428eb7e5abc0710ae086efcd5f29140f0 GIT binary patch literal 1756 zcmbVMdo+}382{dHE*STWON4RBt&DdDxwMEe*p@AZHVUgTF*Sz9XrzR3+q1T4yKQm{ zEs?dG)Jk$G#4*KDE{oXRZc^;gMZ0R-?^8OQ{b$d9=e&PB&-=X3@A*B??|Yz>JOrhr zmJ$LAP*hE&Xj7>4%?)b!P;P&L6+;~=lNGG8e6r@00Z=3%FqokT7084jB3T0s;xII! z25wL^lsx%Sd=^h6rhO{pGHf9OcM~+MKDxaHu{$CJG;by?(3e9C;JDK`KJHBWbu=bq zLK@4R0S&a3nd_h(bc73E2yQYUv}4-ZJJ~yh!06@B8c-d_kk_EFA_V-Mu_7Tqf}sVe zxKmC;nG+$Td5I!~Tz)(^c1fK9yvRttcr9KgebG%&QnI3p7B1-A*cd1U#!Gnv5<-vy zRRr@>P$>Y&?aBPWvU1(uEUnpTqPof)-&<=MO9x$w_=4xmzUcD=eciaA*I6MRqj`^J zvJSrG2%fY!o;APNopj>&FiOZYk5+eL@V@gYmqbx=Xav>u3Y%&?LfY0e&?Q#S84Ky= z1^Cmx=G4o22W#2JU4E9AT@rWI7ZaL2eS98_vgG3|4AIGo2jfB~U@CZ!S_;JK_ar}b zYX(uYUMH}(R0JMZ)+;py36ETkm8Q$Jj3C})@^mNJ){Y)kxoj1~)B3y#1y<$Tnzepa z>teDp6GtyK6t!<{*V~;~Y;h(mIDAfE^P@YNU9A18AZWEiO8tD8QT)jMsRpGqd-Uj# z=wzE5N&pBblM-^El#mS#v5xDT5OttdDpj1B+Oo5A+~MBsCQFAZg8sE)0QE5t; zSfM2}C(ExP0}@FN1zLL1VsV_ajg3Sip+{nBX$7#db%z-NSdsP755s3tpRI(sj z{nbPzNLkE405Gi>k1`~torGz}pihZciD#C_hpb$Yz;=1sMQOiRwl@>gH74u#s>Vf) zY1*1W$KL%rlimN#l$O0%E&#;+a33^Xv?0jS`A}*sH$H9|0QCPDg=VoA2}R%yieDCJ z^ItiMp%UII4ZNW!#Q%&GG6kCi5e)1@i6~JDVzILu@DT~p#yI!|du17WI>qtcg*^AE z&NibER#O!R!|JYIaL!xQ6MGdlm)Ca`?|Z|`@UKxub(!c+606-|AcUW&vk z*ok3oOBWWlsIi3w5O$o*za#Fu3&F-?jdZQ-VDG@fk%DVy=34VyR1NiF%WJp9mNgA& z4u4bdm|NvRDnl#7>vId9Kkm5IsQ;{9wo~zHvg^F_PoZk+U3b8OE)doxo$&pTSA7*fu4ej zZ@pZns%?DBsoRXN6YA9tO0TSdA7Ie>dNlZzp)Jx9K z)lu-wOV%?qG|&ghF$-(B=PQ6E6@v2fOB9?_i%K%nGLsWaQWYFaOEU6{GD|8IbQRo< z4CKUl4J?7s&;Sgg#CeU)4ULR}$kfu*GHQ^xvWZa%Irtb^8JL?G`56qF7`d357#SJ% z7bbCh{1s{PUHYWN;_nTf5igaRg_j*waAve_`S`5WbH4hkZ!OY(3%1-Ym~q^QaU-i8 zyWhJnzx?$h4V0d*mJG{&)WH7dvsh_^WnlHwpQhVN{8sih2|w9pzmmE6*P?x0qFMF# zxDRiW6k$E}>hKDdDb{W^M!cUH(v(j0X6Bmp2uf_fyh$cui^VUdo^zM)y0PEmvU?p{ zbE4+=2JPS_laew6Raq0ITpD|HRyt2pzQ4w*aTcHEQJIx5W97EZn`gjfdG2FJ!*B1{ zGe&P(lblsen8MTpk~+x)X@f+aLO(Id&r>-Or*e2W@MOoJGS!b&*_or%!wcCtl11pbr&l1 z`KPvT`Mzwjc#DP8E!OHEOr<;iF5P&2-M8!agii+x)OERCoc?RO zp0?oX2^-WKe!iF=_0oL)>L>SeHn)n;wY{n0UpdkAT0G0^hUV3?m~^)ae7P~m>;X85swAuG-$dtLsriJBjr{lsYTTZD}?Q6 ueeG2a{zo$Z>cn@fiTid%Y?Cu5kMFnT*;{6|q@Jqkc=X6+U(EMcGG75Um}y7= literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/45d045616ee8db33.der b/tests/fixtures/diff_corpus/45d045616ee8db33.der new file mode 100644 index 0000000000000000000000000000000000000000..94734cc8219e8ee8ec2d4998a908e13805e562d8 GIT binary patch literal 1172 zcmXqLVwqsj#9Xm}nTe5!Nsyswdn)UNGhM%mdW2RuZC~-qfR~L^tIebBJ1-+6H!FjI zkD-Er92;{e3%9Vidrp2*Voq>odY(d1etwC9v!jBpLXfecuz?^*1(z@vLIq5a8ClRk zPMp`s)WFcl$iT$F%*Z?n$TbIZ4L~5uAi|K_fD>dEn=q4;tD&TUIK*H9gux1VWxB6GB7e#2sAQiVpKx*8Y3$Ma}y&!15lic zsfm%1;mitlC%q+k@2XC5?)H;vIXvli&-aI`X7q2^Hs!$WoX^uzHRsP0*i%;}#(XgG ze}a`}3fDIOnZ&<@dP0clCPl%N3DnnLZD1xlN7u zqxx%Op6Fo>b;Hbp#gUy$1MjNjFiJ{2)om2Gy`Q^ph5B(`-yErz?gw>Bmsg85SYKcA z?@LEu3wz4l;F75|&#Jb}XYOhjIh0}IuvN8JYgJu~+`;X8nU;Q^kUo<)==A0NC(qoR zb$@&6bV)d6f4WMb zxZZ!J7Uo&^B`Qjt8P78@GcqtPZeolFhDfvlA24iX`577iv#|HDG#^^OAPj(P< zKV={f(xS{FVIbBZ(*MD`eubW^$~xbGU&*t&?>)LJy~m&$W@W`cU=7smY~9spuM#6O&Vm zO2GPbi;VS>i%Ja4K(3Q#(KpaF&|aXqK)p>BYC>{R4!U-beGq*)28tkk3M^6v5(XS> zZ0U_Wj7*FMJZxORFk@j{V8q49jGPF8`3sl`f#LowZ_)Ri{{z;fmn?o#x!S~C=+K)O z-W!eQ-yb}noL=y`=#E**@|$&6cwc<5xlwWb%%`5~4;_lki=UWY(3rJKLC{}RJz(NY zbykK4JFeKBmGXEfd42B5uEXJtDjq_-IJbMotnq^GD5fdScPH+IucXMR(z){c&^#NrJ zBQszcTpPd;_p0hRu3=M74sFodw02qu6a|q3d3*-R#*;igC4k#}e3sUsb)ckBWQZh{ z5PF?u>s?|g4zW;akC_uA4U)v(a1ui|No}Hd*ry2g?RB#pE%qe@#9gf0X_0D*K&_E> zx@?>$oY=^E%+~N`&c-rkr^FM!f4C!6mbSreW@t|I5Zu`RP=Y>VrI)kpQdk46U+ZQp zXn^F(-xZWemRMq;mUS~iyFeCK_6sg2^6#g~`wPJ|gs9QTkSv50Q6)49KPe;!sS=uk zQfPD3x?Qx>!ZIAqaN_hQ%b zSxQ5L&SQ~Ch;KQ>cG(G6(Gbhr+t`ZPYZ zcXjYwEFtG&&{~{!$YU}pTDV~n8};BMPPQ$QZ42N}&Dq)$+LXi$9EE{}k1@$nimU1|hX?c|6q+)nxvU370T2Nwg6|E?cij9md#Givyl0JO ze@EkW@A|Jy>HYkA&FG2V#aEvkm=orYjVPus7|&e0RK40>P*ZgJ!0w8(alYRUeNz10 zbl>=7W#~USQ3oK6!<0j3>+*|qs+~oL|7tyWqyBF5I}^jh^49aj?8s#Qz8?qoYp-^G zG%}U{XzoD&yI1CL9eD3Wu5)te?SlEj9O`lQ$=Qqbe|O*L{4koy-nH-s)a1y=y%8Jq POUg3!XY((WmJj>`;JZF) literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/48bcbb098eef9326.der b/tests/fixtures/diff_corpus/48bcbb098eef9326.der new file mode 100644 index 0000000000000000000000000000000000000000..c012a304428b228fd1c7c970a189d1d3551c5872 GIT binary patch literal 913 zcmXqLV(v9)VisAz%*4pVB*dCwVka88(RW(Y#cw|YZq^<1H{fF9&}#EIXUoFOY+z)_ zZNSOK9LmBb%oG}IC}to6;&2J``lObq7c02tB^Ol|l)w}*3p2Tz8_0?C8krdw8<-kc zn3|iKM}fH@0T9-}7{lIWRp!C(mn~!KpLky_+cjtESNEM<-QKGU&sHa2tLeP3^IpnY z2AziG^OwEp-Cn`NKi#_d`{E`hRf8rbMFT#dt+M=#jQ?3!n3>ocfEEJzsw_MpFKe?g zva+%>GaB%K1cgD0m<$*UWI;SW7BLo)b3Juue$S}e;9&Il#H!OezWtia8V2$pX=N4( z1F;5?bsqEXw9C3I$iKDdX+jB0#i2P24F*OqlNlLV)C^P%lwf=VrZ%yRl9B=|ef?B( zy-dBFRA5-@oT|6=ab!kZD2+peDZZ@^U?-00X;T0pxlE4mP&*Mjl2cMgv`t zUU`UB3ltW}waFkXNk+27!ay&%D950Qk(-SR=yevxYn4naOe~FmSQ>vad}kz`)92;&x0BXuZ3EE0ZF_vwRteSzpqFX6!L{5{X||y6~gXde$|u8?)u#vOiSs zcmZ^wxU}!HcTc*HJpKQ$oc)%0nDW%q7JuT|LYOy+8vmCMKu-G1=?n%bOa@V2pYNL{ z&E)$#Ma}ZolR|!_8%yuCz5V&~T6CfQ?5>S^sn0Eo7p8l!j6HdGmh71cibyL7JoBKCj?P(MEs=j);a1Mv-nb`p71}7{!7_KwT?@FQE~k99aktBLXcDABezYn2%64p(!%Q?zni zrBz_4R*|IQioCfY8@ZvbtHF|m^8`ZRMjkNriy+}@0~ELl-G%NEAlRq^Nf@66<{R>u zvKS&%sgNb9RSI;5WUv^x51Qu06{|2Fe*s`kS7f2r>v^~oP2*uniGYCZjhlpN5QTu5 z5XmzT0*0Xhi?nAG*Q1&;OByH7RW+&BhH<+FgT!@=YuBw-ns&Pd-?e<7jcysMRIbl? z{saBr#|8$=x^#E&z&00Jt0k8L74-&0UiGrxIlAIMoAU1m=h%DNJSd3l$`ePH71$)`cQ*6hrC|4f&PDf3i>l)S@@rC$No1j?EQ^DlU+6g0vhnj9Z4c_} zZ;9)Wp8mG`rz3}ReIvhr`SrH1Stl+$8WXInAGx3A7;|exUC`q>a1NySK8ml3plK+rw`cH}Fxb)b^pJ%fE$rfp1??{V4|Y+13s;INy05wQ>0 z8Vr_H4wao9zO$|m5sC}aKA1dj<-Wqf3F`-z2AK)NFR4@tLMsIp2Fx;G1VGcl%cvZ{ zrkk#!!!()+1%CAp0qYHBE>#4)$>Fjf4OF_CE!HzQNVFf3iPv?MoSSK9_$vRQ{#ech8qOU@xO`3}`&_#v zjrqRL%P%JT_CE{EPb;369TxcesDC;m{s*Los!xDq<K;|( zg?O%d=lBw&?eDb0skG~WKe7}mTm1WO{@#B@RrTLxnU{EG{PFFrgWf&M-}=7Fv-lfm z8I!b}W|u#^arHQz{gvYYyusT%@Ik|-Pj|_BkBuYwdL>^@_Io`s-RjEmF@0;t{pjmZ z^CWS}H3A>Fd=l0yY`J(rp`Vj@s^7Ki+OM5@aWHSZUArP{P1Z8IkO`MQ-R&oKWyzNt59OBkw+RO7{jTPI`WqmSXv2KXrxM6U5`+?S zb`G%^Loqq-9IMBr&S(Ofh$>#~Bf@4{Q;~a7hyRaiIi=3JnHafK6Ew5L0z5Vj$CODa zu}WM!OWP>a&hA2io3mDpVhIXW+?0gHs(30~OV}teGzHUYbQqq>)1azYX;SKJEL%xd z!qcM~RBBY`8;NA}R133Ct7(&9mYJ{DXn0fcGYglnGOS-IE>lmtr1%V@QqE;CbjE|B zg!!wbjcSbm89l3Da>DYJ4NKxFlyk9a2f;Mb6M3lmHA0^78j+ax8j&o2jnKpyRe)=Z zfJ|)!6vl-rRK|*NKPsh>Lgt!nF4qn){Pi_|w64ssZhvuf z_ZMb@^WOWMztblO>%QQg>3wXXwa)j_PwxM6@JcBy!A73^0AUoRdz5;OWK6O?PGwmS z`xQ-K#m=48t!JK=*m|*a4bgwvJC|2EmL8SnKFb*Pyr znwE>2%U-1YSUOt0e<(vDIbdS7q`1Fje{{yloGl=ZCG{&K23J16VT_OsuO?n!Cg7VqB$c^7jYQ_k}T_>)`3oNmS{bZ=_= z#lLoU+PUxgweG3rq;ucNuG$<9W4qvus3Y@4g2L_%@eL(EN-aL$5zb~)ehJ(p^lEmE zSEV~y@6Ug`$>D>Fj?h1m2ZbCz4^*r4+~XjZayhZW^r3}I>5o~88ho?3b6?+YYi>Ne P5bB_@J`(n;Y|(!KXfu0m literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/4bc352e324456ac0.der b/tests/fixtures/diff_corpus/4bc352e324456ac0.der new file mode 100644 index 0000000000000000000000000000000000000000..28df0a49c361cdbebc47431596d143516a5f8b45 GIT binary patch literal 918 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/50f6e40f406a9583.der b/tests/fixtures/diff_corpus/50f6e40f406a9583.der new file mode 100644 index 0000000000000000000000000000000000000000..eb599b72df793abec1877d363ebedab5a65a801b GIT binary patch literal 2755 zcma)83se(V8lIUtw?FB0jZIPk6KRcVyRV5Ta~tZRSCXQ#i;^XX-UytJW|p%I6>H!&;5E zbYJbeJ5liH@@KeZ#qom3zRKWy!%x=Ftsu|LV*GKc2rmW?alzfAws#LLaJLxQn@hYklUes*NJ+o3g6TGn~y(;eR}` zs*FvC=AlnFn=E673RZL0kvrm^-sUJ^q3YVd>E@tsCAICnC(5Lb?Ebu>B4sTy@I(5@ zmj~;u!>bSm48bIJHZ#F&>gh#HpRxYSP7}4Md1aQ5+4m@NwOH5W*uSCcvy7RRD@W^mP{*5 *_3H1|4CTls#r^f zD9m^vVIhPD!h~sxgvErysBx`pp4zNb77;G`JRN;e5){WtDsA%I^V6u)kT&S4DR27L z*_$?>m=ul7J}EnOnFzz2U&P0V!VSS)kO24c=!uFjGv(`~!vWDL6eTY1oSF4t_i;pf zqN^ghIw7GfAXG&pz3vDDDT-3cFr`>2Q3zEAQfJZ-hM6JS=^|G;h*AMDwo*pSLVsNB za#wy-G_I21EAg1w_|!62GNvFTxI`t9x{>KYJyEwprBLEh922{7Cto)(6nMv}idfkiqEg93Qr!)L&lLCxtQZG`NjT#+cbbc>DynSyHpwU1Eq%2m! zW;Tq#AOb95%~{TZnM@A^_77B>O#0Z!Nc(bA(wh)YpHTK)B{DH13;5XmxiI_)MHvXQ zesVKhgxZfDM+62ueTRTzJAEOl2Rb0Dm)48RBOSqULy^=q`qZg6dFUl3$R#40dz_!cvvv&sp z_Vcxj47( zb>gcNiy;WM26e6b8G=v-lme0v0jJ`uEMcr@h;>Jpr{M8%GLAn{*wXxhs_ToD`!@g6 zh6u<-isJZJyLz7r_UTx4bu{~498a`f5Y)HYXO40)m-8{5HfWD}#m%uh$(oFjz?#af zy_Y3(FQpHB`{|F=%V{S&*X?nny*iV&a4@NR=gzy?;jP}uqlTs|x!hx8tT=#P7vhGT zrlC+oz|XeTeRy)+wNr0|#fQ{(RS&)v)L(aP^Bpg9mOlJ%z6H=TW9isbyV_j?+G4FsmrT-Dp$UZ|7>SVi>@&D(5+)3n}&(;_cnRO z-{hQIQ|kQjLw}FaA3rd(B>dFQxUuJsvSo%c)9UeBTW#3TgTL2~U1;6X%`Y9%m#5am z+*|GOMRfkvu;<&3H4BH@H93J1Va6TDL;h>4PDt-lM!X*fHcoVO{SX+xedOcl(oY3= z-LtYkoj$dvImVKalzX^go~CT)*1;2B%F)B|fB5>s&ke%b<-3l}{pI}}{vSV6d?N0N z?HXJaQP$wy^IaNbGG%lB5lu2Kz4``zGUbEX9rf!>XG*31$FQ~K_ianDo~^B#h*xgB41FF_7f-(U%Ih2Z8rOW(o9EZ`Qq`MHwk@f*I|d4G atVmd}UtBvlek5T1_Re=SOUgcZz48CS8v8c@ literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/5132b2bca3f1bc9c.der b/tests/fixtures/diff_corpus/5132b2bca3f1bc9c.der new file mode 100644 index 0000000000000000000000000000000000000000..a0575de9bce68700b0094b555c721e36b4c27736 GIT binary patch literal 1134 zcmXqLV#zXSVh&os%*4pVBp?-hMv_P5mp|tuMQxXlwuT10Y@Awc9&O)w85y}*84Roq zxeYkkm_u3EgqcEv4do1EKpZY%QTP1(^qf?MkfPG!5{2N@qO#27)M7&(11^v}voLdb zkdc9$IIoeJfw7UXfrYW9sc{sTYi4K)<{CvANEu2Ph(Xlz=O$+6=%s@V)l1ILHHb72 zW@87sp!OY0c?8Lwlp}OJxyCXnC!o0vijnwSC%_<&Z+@-s62 zXJKJxVs8Li2;{4>fc(Ir&Bn;e%FfJazylH#1}S1PU@(vc@%UK8SVX#Sr;74Rtg-Vs zaOPrY;hqft3ti;~@*rtt7ND;hMD9t;y{#0zVrQ;$SaJ6o6U$tuO$G)LFq0V>S$qt< z3>0B}1I9MdjFOT9D}DWZy@Kpaa8T%%7a19-!j&+!NkWxmqA1ZzE-Eq50a-1`qH3Vr zgdUbn0-1S9`4z~a3-+o4$g2h%Y;5U`Jd8|?24)~5 zxoMF}RHdVFnWF*F>Kub6Miw?MV1ThOUaMqcVPa|g%F_6W;R8b%17G-*J)hUDv@@30 znaRHS`-J8GkX<6tJAARud?~{3-W<{QL=gMSl-Edmuut>ynsJ+J#_69|1 zI0R}4?vvX1*D0{%YNKxMUrx?rUDYBkd#6v{a4`{R@3*YVFNqVUH@f#M^?noikK6sg zoKsODiaTo#Y>c~jb=Oj~ti#;I$jD&xNup+3&)T|&50gu#2&Xe|yL(_^#NQqf7yUEm z1wB@k>+*Hx?vLbSX#Lrn(P6P(rnw0AE(#{0W$vQVD**!EVve{LT lm2HCR>o;kR`<`xpvTj+UjQhpF_Fr7*R(;8H>v(ZR9RPnZlPmxL literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/5186c5ec3d3e7b93.der b/tests/fixtures/diff_corpus/5186c5ec3d3e7b93.der new file mode 100644 index 0000000000000000000000000000000000000000..1fb0ed5f0846938a6f7897c3b483a705ad48a76e GIT binary patch literal 1681 zcmb7ESyWS36n*#QB?gk9G!YO40zrrj;U-4L2qL436dF-sF^D0AP#_or6ve7RI3{U_d;Ssp{B%1_v2)Iy;Kt)6O>ONH*~ZXF8`P!h`%8RcVq-H|0!flReRi^d#dd&p+HWMP zRlH1=;^ycm5K3jKERiI3jOZv8rbtr7GKnE7+` zJZ3|Bq|UZ7%chv$Pj6fzdFIXdp2a*?a-Cg#>Db;gd(Q6biVXvojGB%+pS(6}kuWoL zt-+zJU2M>B#-_>bA!uVw@#cbuK-rrm!GN zuP6RNPFM7N-Tsg*8!EOfKG4wYnu>Sgambbyj5bI)<$#7Sap8_)LMz+00Q>Naqdu#bB1Ki3{9y3j9@54 zps5y3!L*|#+7SRAKMn`J+|W_yn2y(yF&v67g|1q!521${V6<}5o_8Mj z=W~>$JxMQR!Dc(x=QnG-yn>*sc_0it$V4#|lY=D95d#sDjy$-ZTXENr|1M!pscPYD z^hmd=*}h>@<3Q|9@`g!fB`AV`!V=|F3_&m);tzdMx_;P;1N4IG_FIT98|P>CK2+^@ z7um|pcsrX+o@=k9qZUZ3Ec@XCshzv4MbyR@J8HvgPce9b+!B4)mL~;syp^Xkx{CvU zIkIeJMl|??pw6S*+|^B*ykfBS2a};8!wmPGIklg3ZCk(SS|D|ny}ggxmZ>}4J;0gU z)w!F8p3@Gx{Cr$scochlx`~0(1BIS?SwVB{N~Anact=)a5A!l^GxA+1Z$FW@Zs|e> zl7<8dnhQFMOe@#_ZMJd7lfzpoefBsnk1ou=He6PuzEW-dN!PA?RM!vspmb^E{Nniu zE$XH!@`)Nh+WtCf+?8_%(e><)gE4=ah5d%oaXtZSp4jq&Ye+#6Vm05IGVr=@+LfIi z5&5dCon>tkzg{q!C97F50CCURXJg*PYwsY+YaOa0NvhR)eQPT`gR^^LP7k&BUzpJS zWA*@G)~%l*3-nD&j@z#%;_Zo@UN@~3uU!~s6PK-epZ*x_%2+5{G6Drn;e z>zA|dI^hys*8@N8qTnl^>9G&Z>zwJ>VO&j8jYKu~ ze^~8qX%eD;;oUBa`%8~8$T~d@cPbCQte&`lYvtt@T_}LELesYNvb@p^hc^u=p06HW zuNaPYVQ_7V5ylJE#1>C@*t*zUwyiHXT$H!HXV%l>goG+4?M{6{TK)c(txn2o*}tx{ TUztRvGriTmyC>(lC2aTyM~P(u literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/5338ebec8fb2ac60.der b/tests/fixtures/diff_corpus/5338ebec8fb2ac60.der new file mode 100644 index 0000000000000000000000000000000000000000..f1e3552db0fb52791734986d9039fc0ea016e5f5 GIT binary patch literal 1122 zcmXqLVu>?oVs=}=%*4pVB+OncWWhZ*tz)sZ+|B)S&MxcY<2T@COBoQtW6k&)qbq0Y+F@*NtJj11Oyh4WWzH&>k++P)?|X8L8N6So$Lq-WQB zNWb-nS!;8cQ0%d-?myExmQSvlI!S_m%g>(zX0z(`8@BoH681YD8!Y$W?lfa|h0D|S z2t6qYa9YSJT)fqPwu{O?-TJqClv>}IDH=~NWjm)X(RAnW&7)21y>V)cYHI27H|>Sb z8|}(oTaof-)qg%F8}|EiSSyNp{=O>R&aBaL>qQ=W&xL^2_l+T&3iICNCcW>Uayahg zktG)<9?(;nIOEj2U8;Roz7=l!9i45#=3(yM+ji&EE4JTd=C$+wJNfxWG)+GLsPIt5 z#;Voc(NoMj8lT);ud?n((b3f3-dh5nGchwVFfMLli~@#8n1K*5BxQve8UM3z7_b2; zCPoGWK9CqcNQ?!T9NG+IL3~vfF#{1c4sA9@R#tXqMmUQJBF)Dl#v;#bogmz5AP>^5%pzeR)*#X%ai-Pe%^sd9Q}j3@=N#W8;K#*bPz^JSk&z|e zAlE=2#y4PWQ_d(UDX`Ml&rdEc&?^F_pyWhDy+m*dD=8{1F40TQ&o!`uYhY^AgK9`F zD#4{*A7)N+YLS6G$aV5876xVpCJT%f7$6ytL)d_#90OsH0SYWU23!UlY;5U`Jd8|? z$mtK5SAgk{kzp;vJtkIVJvO7HE!i8Vh&>T>xTpOq@}TMa2dnzE9d};*+-}Jr`^k3e za(5m!wqp}NvxhTXdAnEq;DlNGJxUj``y1T#%5vJuYb$zy|6fb=F`JoZ!h<&6$P>Cc zZPH}#DNos7bT)BhRhe9wm-5iIJ!{g0S;-5%fAnvAWpbHypTePgY>#^87kAJ1wQN0g zPSR{{>&ba1e+1ZXtLs?!{!|NFcfb~|m}N39AOD_6bV&G=sJe>%%ke|SrB8UcoqAcG z#=CgTjIQo%oVL7Ut~YPe3zj3-|804(DfiTy12TIoqGj*5o{O$}?CG7ebNSsbteZU7 ZK0GRtedx^Vv-Q^^GIt$TsH@+w0|4|}kFfv% literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/5539f8c901051834.der b/tests/fixtures/diff_corpus/5539f8c901051834.der new file mode 100644 index 0000000000000000000000000000000000000000..ada5a02a17639f66f5c7b49fa8a7fa13b65bb166 GIT binary patch literal 1172 zcmXqLVwqsj#9Xm}nTe5!Nsyu0S;mET%Z2Z4RsU+Ng3st0@Un4gwRyCC=VfH%W@Rw& zF;p;+V`C0w;T9Hm&&f|p%n8m+&r=A>&o5DMc2v++2r@PlHV_1<;1cFSsDKGFBMTbH ziSrtn8kiWG85kRy7??!?x#nQ50SH7HL>O`#aDvQY6J~O9HIy_EhZrn?Fjyh4Ot(1E zP|rXIB*iSOiX`P&l9N~rbdiEfm_mq8Fvx*M21cd|fyM?+j7rE}V`ODuZerwT0E%-l zH8C{DUjKlLH>ZgR1;imd^k^Ta9U-P=BYa&daptGe;D*zA)R%zD2cy7%Hn zJ6A!z_rln}J|E0gwPOB1zBD5)cB{(wv>L@nr*8N2?Tg)aKv2c`#QJZ))c*ZWe7EuC z)nqUE&01m)MW+X3XfRKBtgEhjbCqrhNy_NMaB|G(2rh=23)K1H%nQTYdZ|@M|ZFk$4le_d>_SO$>->wE( ze45%BvT%|?0?Vy(o{H-o0-7g;>>i{VzBsU8Sw*ny_n0s4k_WGSE`Ij%_Zi_(r&X2R zCerh*$`@%K*F5?#Wik^pBLm~&CdPPRh(sIk0mD|7pONuD3kxuvv>C{P_^K>o1|n=6 z+H8!htnAE;a2At+5J*}Wq@2Tm4M;IDGC;KOv52vVT;B6Myy@A2xK0ECwb^z2y8{{UTt}EHXB*hO1|4(}$`rPE9T?N=4U@oS2+i zR07tgTV$-4TvTFU26CM|i@t%bf%XE;1?p|8P!p1ia?rJd?1SjbF;E2QQ(%!YkTBq2 zV@q%3VPs-7;9=teh8YXv0wXR)X5>T&%wNDn$jGoTF87|`Cf%CD+h-))&HP{VzHE^S zqrm@t5scXzE-tI>=C1UMzv0G`ywOd%TVNLZ@)Yd}4Xg5!7VVt&=&@izsoK_+XWuVP zd8@jnYGL2<-*@`IEr0(nQjYyg*@tI8RoWX9iY1@!J8}CN≺lEz$43%S~e`VoWwQ zoc@M2rFPzNtEwZFN#|c)PuSx3?O1M_{=>sR3XG0%a#>2s%$}lOST%k2{ebE3&s>_H zx&Jobp;K>OB_8q0n8zg`C%P-Gx&NQ+SH-^Ino|udIu@KOQr+&<%&poe6XR86C$WC! tgmU3DO+}6kFXB#Ts_AENxpDAtT=zMk-KA~$X4x8>3pMX{*GIiz1ppuYpJD(2 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/5602edbbca1da24e.der b/tests/fixtures/diff_corpus/5602edbbca1da24e.der new file mode 100644 index 0000000000000000000000000000000000000000..7343e583088461ebfd31be851593f7bf37a8d25c GIT binary patch literal 1634 zcmXqLVv93qVs%@<%*4pVB*4B$YzOaz2Di=X5z>5-ANUM-**LY@JlekVGBR?rG8jY} zavN~6F^96S2{VNT8;TkTgE(BmJT95(na-(2B?_K-$%e)Th9F^PVJ*0@drp2*Vvd5l zkwS=1utHFCV=H=uo1SA$^8_F6;LmVmMn_7~n5Rj8t z0#u$`tOIhZo}q++7}&8q{91aMdBr7(=|zdTddc~@29ZGDu>*a7&X$RhjRVYJW@Kk} zVqiJC^uXF5k>)`QL+%?C{;N#(7+G1_nHa^SGfGMdtn~HE%ggmrfJq`5m?S_p={pAm8}Nbj^D{F3XJKJxV(tK^ z9a&Wt9s@3z3CxTJjq^dOK%JVOTwH)+fk7Z#6;qol z%tMKw04hdNs86h)N(^{FK_v_dP$mNggC@oXHZEXHurOY$WMW}rX<{q@;vB{-hBAf} z!6P|Kgx)Oglv%{2$FOI$zXtmiIaBr3ul`+SK3RMA0|Nu&?8h3K^$ZLw%na@Zu1tyy zZ{t?7adJGL*yW+i;ljuM@14bRHXg?OYa)MVoS%2b{y&p~)Gw|j+{bS^tX#oUbM5zp z?OX?*ygcq!=(qO3vG*6UzV$GaGl(_ouKuu6m7o8E)l%30C-$FEzR>RdjYU)a$8&jO z;jh1-_Ev)J^)PS)+I!~sTz1{xp$*sfE2!^WqFcJWJK>i*7uVa}CM^|<58vDZa@rox z&DHlW{oj7`>|E`uGV9`EJiq=d>*>Cuv}t!_(bF41i#_Me&b%jE^X8A)y|e8p%XUPs z=W1_|ycinv`{irRyFU&?EY=c6viM>qH^2NUxx6Z4k)~(Ymt=}DF#0B*aC>9&llL*t zCJRHL#hrU*6g8O5Nny9xoF<<0|JqljZIetCE5mOuf3f$LX4nq2;*z=oX% zUwVx?-@UpPq#t|W>hB{r_~U&hEbBSH`?n$6wqHwFHyo**VR@8GEqC$b4}0d_+NnQt z>y`U)G(Ln4cZXx z8agZGaDm2^nSn2-$$5T=`zLey;{`q^kJY!XtnE0smDQ<~tNxGg8WZt19IyU%$<5u{ zCbH}W%a1c^`o6RJy)VUjRPB(d+kWb#&vYld#jgz}g@s7g*Cq&FG|6(!wMBZ9?s;`I!;*f_M>JkHs&Ff$wY8FCwN zvN4CUun9AT1{+Ekh=DjqM4-!*m0S0D+NN2zP;uqQrL_(@RhwV^{Pq*NnbHTuejYFG_k(HI5 zk&(s0z|O!1#y4PUGsq|@DX`MlPfjf=(JL;_0fn_bFbsf{E-=V-LBXvH3Ts_p;OhZZ z8Q6oYQ(&<$Ff%Y=V*xT540H^%U`8@AiphbDEC!lcUS1A2vM9A69~hMRMU@7!AOloc z#0*4WnwXj3EJg!IkhDCDl>yL(1ttrO5Kb)0DTbSe+lfUv5F7Yd#8^a(79GD4(fz;v zfX%1SOyf7_&dBeXZomh!fgcozEWk9|hMYo~GZ_rhm<$BMOZu9f+>h26fjUjS-ZA3pVr>*?jpngJOqG7sh- mJ1Cp^Uh6!!!^Oz-O*)(cQF;;6uI=TQ41eFkD(20~@fQH%e()jy literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/5dbeb4d447838c7e.der b/tests/fixtures/diff_corpus/5dbeb4d447838c7e.der new file mode 100644 index 0000000000000000000000000000000000000000..eefd9673fc753a7ec0728950b85bd62e0c1fe226 GIT binary patch literal 1535 zcmXqLV*PE<#PWCnGZP~dlK^wrXOZO_mOZ;z)&2i?{Fi70UN%mxHjlRNyo`+8tPBP= zhTI06Y|No7Y{E>T!G`=mc_4>Nn9VUau_`~$P{u$CB)}{z3=vQW3U*X5GBB}F@HH?r zkQ3)MGBYqXurL6FC@|O3$k52x)Y8;4%0R|Y%0L35Uoa&Rnft|&&|M9f3_iR$t)T|KqhDj9_QPI=8GEC2<`_&|AHvBs5+s&ze{X9$iBRxm4#Lw58 zyiaX>d-2!0BOB6FYxd8mm>S0xv-I+@w4-WoC6Z)L9$8%dt7@a+?<;S7);(V3{6<(g zx!K_!OYwrMnQW=gLfnq8z3!{`^_t|tM~%K`uBN2%vAX9?ZSwuH{`ISE51;TZ{P#qm zf7_(q=dR!0+0=^a zED{D{4I+&@_>#Lig(RN-vbGMGI{(?z3Bs)gvLFR~EMhDoojnB~RU%uRM7HmK#%L$# z|NFmrvq3OOT9C!Zz^e%}E}G<%bMi}5(u(r)N_2}$5=%1CBCAORo2Y>>$WR3q9s@1| z4mP&*Mjl2cMgu;OAU`AHe-;*ICe{TIMXDf0Y#iEbjI6Be%!~%sAVu;lCI&_Z1`G5S z=(K5Nl#~=$>FXyK<>(a|x5XUaMqcVPa`w z%md;q#teoshC_yR;jWU4UEkGP+}kw4@%8aTicw1Y>#uHITQ=(^U(i_w2F5w3XKuO6 zz`(-H;BMf`q{#4AY(iFarh|~pw4+LY7b%=QwwgJ^Xx^=34)b(3NJcUnFezk(Zf4ao z=ACdow##(;6x+y~<>9v$E)hsJ&a^_9`$Rl3Vc7%;kqc+L=NV?WD4^qH4zqbt+V+`91@@(=mwoUtbPf0BP-2P7wfc8%Db8i3j=HUCr&yp%? zD|W@i^nR$kS_|T7JZHAA%X7%gv1)kIX7M-;$z327p=+Ef^K4y3F>a`ZT<_Yev zn4?+Y#r!+n4{oxA_0LtBwfu9`J8$1PrU1qQ@-m+nI)+yhGqsPAQ@(1U6@QjX;MyR zvO;ibQCVt{f@5h(MrvM3W^!UlW`3SRP=0=iLXe4>ft)z6k)eUHk&yuyM2Yhnn*q6o zM#iR=rj}6#3B)?U+`ts#06n4{;Owa2AEpolw5*A75wfotSs9p{82cH3;$2KljExM- zoORVpN?uu+)E@7+aITDf(>{M?Imy%CSos`6&pw{oUH#JfxYGOQrf;`JfANg1&yIBX zenUy3Ev%QmDX>wzuEs}}S2kRRu`EWkLvrplr^=e-oyT@BnN)UR)0Mf^39%;* z2uI&|n0?#z>6Yk~ODgB|O5R-Qdobr||C-Z!-CO)37oK?ZWxnr}n-{Bk%3M>61gjjb zXhc5Z>isz@)T6~jMoK#*?4Y(rY`UU&;|yMp+m7x$SqFMfR(wiKyeAnXymg=d{vCgx zAFwo8elP2v-Sf<1C$En}^ERG8HLWpHXv^!Br)s|Vci-GQp?RUbHZJ*tSP-hG21{`c`>5V+V3}p}kl9XrhHSjj@T;RUIwaq!Bq@=(~ zUq87hN3R%?Y4nowbM>LA6p~9InFKYDfU*cMhv)${8Z@@R>}OBJ zAa^JD{MxPgpq;!r2&Cl?nWn~0oEfQ18d6ELt?r(WYJZMu7UcK-LR%8|z1C;i2g z_NRKWU%owmX7K9b$+HvcuWPwZsTS#9DQTb)wEEqCz8(21-v&E=+A%+V^@?SS8k$bu z+&Ww9odWA&KjFoz&lZa}ZV%MkBzwoX?a$mTf`&|o+87yDbL7R%_kVokhl4}Vq+?S1 z_sx-qby;95C)t8;<*YV%IRQDuf$L)pc zZyi_`8!)Y@`SRjUS+9Y_7r)*Lx#_#wjvUyu<@U6|SwB<@UEHonUA^7yCR6g@i25Gm zDz<4;g$#`*+`OqU%`7zO{sPU5xe-n`jh@7IKm8te>f(ou#XlEa=`e|RT9J0Rq_C%F zs@Cs!U!89xN50uFrY2S@RB?XW>X`o>U$sxaaZbL>^!kzF)JKyq-xJlm7`lAZb@|?X zhrZff-x;_l#B9de-hTz&Au>`*60;p^EJB+$u%BxzUOwrS)|%pbTtW>0gIja@GUQv< zPhZ~b^E1rY*KE7v)AHqK{EXcVR!7w=oapf5LCpEWh^PrCs%LyLORM)*dX{^XDM zQe7**uGE>RJjqqFx%Rz*it+X5ryIZjd%E*3+kIKhqYN^&pTA@)&DwK^*`H6OX6}yy F9{^dAdkJhQB zm>8ihVP<4!c4A;z_o=yf!K%%f)xv8H81`)}zHQy{b9%dMaqIze4=0}>gB$-2&&oX& z_Gspl`E6&atKv^hUTu8o`JM}$)mJAs8!djlxQQv%pouBbfDdS?EI%XTe-@x0*&Bcs z0{N;eAlGwfvoW%=vNJOp@PGt`L5i3R7z|`VJU$jN77;-wgTl)en}4-MhwQC$J*GCR zW@EpBJV;uZMZ!R=L1aStB-8XG|5uz@`KTRk}o0Cy&*7tGt@v|rjG$~9q0J81}68*I>UQXR5yX)xWFECu`4sU|?XJ^Y8uN9}EmE%nWV@ zE=&r}fm19_+

$Zw7bWz8{(U7x_)TdV_oKDT@b}7#>NTux3(76pWIWn7R?Zgu;8pUv{z zme}*3T6qj&?LQ8%wH^j;Oo|K*R)_hn_2D_yfsSm=@Yxv0pB`)Zq*nF~9f z0hR2H&at?cHO0GRd4QAO*0(7=)0WKE47<*sHu+8HCykyW2=O+xdCgL OQw}iB$(f#3D-Hml-$1|s literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/71cca5391f9e794b.der b/tests/fixtures/diff_corpus/71cca5391f9e794b.der new file mode 100644 index 0000000000000000000000000000000000000000..13d993b9e85009d8c276340f3e375fcab5568907 GIT binary patch literal 526 zcmXqLV&XDrVw|{unTe5!Ng&UC<%#Yo+g|Lp*1EB1)4`brTx=X#Z64=rS(up(+zq)6 zIN6v(S=fY`LW2#J3=}{dE@4Uc{QUHsRE3bD(&7?@;MAhB%;eN!1s@-0LlFZZkQ!!T zZugL2g`oWW5``cW137VCLo)+2BO?Pah!W>DM&?q_qA~*kHg>T4nHbqvwHsL&l$etk zSU#5&FVE;sSm(UM^k}0&wYTTo-v<-VJ-g?&H~VU$gy}kmyT#u-CmKY(wt3fDv&f;$ zdgZYl?l-iaPnwuj{BmQF<$TTgi)LJokBewa`YeQ}W zPB!LH7B*p~&|pJ30~rv9OIXxBKR-PuRUxFPw75heIJKxOGdZ=`kO!z4D97hkLY>0Q$j&c@Ag_N_qsF7gw0FTd-5e^ zZykSU(7j~;@=5i!_S%zte*XH%IgME*=tBnEv&BtJ&IV0P_6B@FTV?qf8UM2YJ;~kx zv=GQwWdS*#Lz|6}m6e^D(SQdeC=61>WWZn`3*zyyh_Q&IHN~|m%r^Kq-&d_`*Z&4$YAGn0Q>7{tL$W@KauG6*nGhw%*< z+oUr}N(!v>_4D-#vNQG4fkCTZtY4mLsGm|%ZlDTR$kZkYRhWrT2vnw*TvTEp3UYuT z3!ec`6LMgJeWn2NnE?kITY4i8BNL;68Az=>i@t%bf%XE;1?p|8P;-+}%+)t>cQm&w zEy#3D$_Lt!W6;FN!o~#*9~Q=Il}s#5ERA1T8b2|7U?^kY3!k#*^SYIG#?m@7**AZm zaDVn8+d?DNyNWAS*CZbGQ)FOZoO5^Yt7i-hEX)k<2Chts488i-mF6Wled6p{zgwHr zUUzH3-U$v|tkaG&+7wLLziI)Kg6f@=zrp9^519$<+r5)d$?vRm+>MQoZvNP6E-4jI zF0lz{?`ezgqMbD@3ti%)58nNKGVjx@$g}QTnXIiFPKz8CiFgjN_q7MyUIpW~X$pp6 z%TmtlI34h&BV*F6RhO$wrQR`LsN)Y*)z}{nblKGw50?AiTA50ufN+4gTwuIcj~Fw?z`krA{^{; r_}?Wa1p(Dtovp2j9pB~JB<{<4_H^Wa_}Vn_dyLpop<7Hp)|~y zRs^I>VXzg%GCBmX6a`xpPzz5Kt0Gt*L#Y-S1nG?rTJ7puuD4cy+COt_abh`Pe6k~l$K~)Cd=@9#bR$?VTgMSef3Ewud6QkJWG`>7}2IfFlTbXJJ%uE553e3%|0^g|CRP!YjM7#^U6omp2@uQip1S&EVd7``* zGnCDV4t;N2YrzNqi;KuOAmA}6?B(bwAsjXk!}@RM4a-3|F{}zsGar5RnQf`p_^}-i z*X^I!MLKHQIHNwez2QE&&pGQ*lQi&)@2wwFPG6mVbc|J>dfA?te)g-1)J&iHyi0Rm zgne7uc-Gc#x8-cS8aVlb+?R=}^eScA@Xct5@KFvum}*x_5incm*T+VlPD*+C%4TJs zbdB49`A883_3k<%fL=hCMh*k$^Q+F!5A>AkO`BMTWjmzU&6l;=?$(ka6-u1b0r%L9{ki9F7?^jTJ;(&06h&h z8k273=!6rws&E(~JB7F=RXKz0B#-`3U}OSeQLCnt}Cf$g7*@~8MRz0Ny+RXq)ra#>=QK% zdVE&2upfaT1cotgS1kL2VN~=Qh+YB4vIQV1zMt~>TpJS;p-}j451DWySsZSxNeGk2 zk2U$D`36~BGI)HJY##qVL=Hsn0Us{~#O)lch{X zgTX$afX9c62-qV9Jn@938|-#C0lT6!V82Wv0&AJQuO3bqktSYDE+>}7=kVevpYqv& zB*QU4R{^ag4IhGZY}Itaueis_E#{$4;oSlf6A)z^4AFNx>)n?TC&pUFwf^I*{7Lti z_;O}$@SUXG8?L3r?&b4?mk2SnISUa2Nj&q|-de=Q%DT!?clAU$Gc+(d<~S6i7fw0l z(V3`l=4g`bOrz=OxLHL8t!F;YK+SdQwK0G97e?y$PpoR5Q}}m!WR8w=&-x~!lH_dT zLbwzm$<0Jns_;1XB;p!jYyNQR!6(v%0Ha`kBjcAE=8Gh6<3T~|$%CRS|Mh1-z~78B zOw|pg0o!yX4Z3oX;s=cPVzHt1^O{HhaaSyxq7C z*-%$ZGWz~Qj<~{8vO$oZ_L~*e%w}O<+fCNBS>@h=TYGJVFXu{eMK4Z#*IRSZD>Kz^ z_pRIN?ue9gZnFA`K%{6+KM+u|Juc7wmRLyi@Ho_Q*>^)z=Fw(@LTj}?&*8M4Ek_-0 ziU~je<`-^R*p3_j=2E?aVnWE~{^0qqHvY@z#xA8TG$*cgZfAo*)9i5bPa03Cyi+5& zub-<0Ie6z@lyx}K>k;_!qy=>_(EgEh4x1Lv6^wrE2>FTAD1bc`ZZU4JiVpiGLut_4S9f?f%444%;Bzv z26E!OMrH;^M&^b_#%9LmQDCmAk%gf#kZT-eAYv$FAOKO%nOBgTRjij^lx`4dAk4-N zb^sG2)FsS}?95IKEUOnOTz~#%$L~MZG2Ck!p6etViS9d`DA|#o?;aiaUs+f@`tY%H z&7rTeW*n~Q{_x=Hr)koQ4_p#mF!M^mNnK-=6N{Ue0u7p&d=2=3mdf%oGX7@)`jNc> zXd#fV$^vpdhc+7{D=RxQqX7>{P#C0$$$-H?7R2LY5n~ZK(j0sI5`XPo=UT%Q=WdPN ztsf4&Gmr;KE3-%#h&6~zD4%4Se&qj(Gb^7}-)yL?J~R2Jg+Uz5WJX4oAcFt{br|1( zu}wOoq@=(~Uq4^3AUjhp9T>9u#rox`hWgnCp$4jOg-mUdP=%QYg+OI`$wegwiXaCF zvPc<7G$F#WiCaq#5wKuiDu8@xz`@3r-pIqq#Asj!(kIWNZ=h?Sy+Ct;dYdZLj${-& z^v$zMQWMQGbVHM}f%fGXG%>QVaRI}Mh4ES?6AKed<9C+EFASd;${F~=r|kK>Zl#^E zw9ZWS&EF^7pMA)-&`9;J;!4#uiAVhu85kJn%nvv{n}LCanZd)rjY*MVon2nj=AW)| z&AU(avFA^ldYEB_+krX1|Gn+~>udNXJqD;`S?6hur1_oyR<08-;#je7qgm{l?Cm9t z(zTZ*eogGv-43+)w8eMP&YG5mF7eR^@BTiS_i0w-S$D2X*47QDMGlKZJcn9*8_D9Y zi;VQ1&6%b=J7X5xmU(}Zr!}9BG&*?1Xi=Vj_ZPLzCxI647UNHmfA7AOcfQBW15dhwvLI&{W)B7fSD>El_ip=l`Gzc8{vb`Uecg%eZ3a~)AFZ`# xDVK6{TrBKNWl|`OD_XkF^+1yB$Kp&L^Yh1F?8(}^cIM%}-FKu8{SYeI3;>AhIM@IH literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/7e2fa4d1f1694fb3.der b/tests/fixtures/diff_corpus/7e2fa4d1f1694fb3.der new file mode 100644 index 0000000000000000000000000000000000000000..c511b9e680165fa1a07767a211c97a23a9328659 GIT binary patch literal 1781 zcmXqLV*6;&#Cmf9GZP~dlK^Mi_GOFi3D_;0zDv90$MJXrUN%mxHjlRNyo`+8tPBQ` zhTI06Y|No7Y{E>T!G@v+!XOTpFpo=SdZu$~QHg?QUb3OFfgwnkSy&4$?4FaKl$fL7 zZln<66RZ#v?5Ggz;b>%PreI`XWT4>eXlNiO&TC|5U}$7y00vQDu8EP6k+G?zsb!Qw z398k?27(|vIE1;J6LT`t@{95^6Aht)?802Wsd+j13IT~l*@m(P(hx_A_@@=uSr*1M9y)0mC+c67`y_k`OOgBQ{BtI++<_M%t2n>b zFf2>@;uZ2qcbC}Nk9{Ws?F0JHo~W=cbX8r{wA3$nMYh{nSLe@5uFdh|e{n4G&biqo zWlLHg>F6|CDlF*yvXlP-cmJ!J`2{T2n?>&WSL}Fm`81c0z?`i0`&8wY-uW=2^z`qx zpvbe29vxh9-}#MO(U0(RGio(I&6G2Kys6Ckf`XL9)c&5Gf3*JYGyW8$mw2y)G3fo1 z?FTKQuFQI0cjdm6&pP96oL6{d*M2o_d(^D^?PN}-XuaJ2Y?}a!+d>LcBya9b-zVgw z8YL)mRpRgN1x*)h9xX{N3uIzuWMEv}#5}{GiFt~FJTN3>m02VV#2Q3OS{n|e9lvjW z@BCg(^Ht2U``(@vH;@G>;A0VE5z(IbrLXjpW5{uXnyD?@VzN9;CeH&W8(BdXEdz}v zY_ZWKh#5h4AQKf>%neKpjM>=I8+m}q*g(@josC1Ajggg=orzIQI-{hdz)D{qq&Wqc zPLqM@6coAo&H=#&d?5Y&jEw(TSeThu7l0GAtSSqS0T;{!W=4a?`5;yDERE9*8mAa| zEO1-k(&hv;HMuCq7}Z!`frXjD-N2PeA#QG4QmV`m<{X9d@s%>yD-RX@-L+xqx12P; z?C)nkmdP?HGJLzlrJi)J^XI7!D~2j*;h^qkC%?Si)DrjQ)6F8qEkf2nd&Qb{SASTk z%FqA7YN_l06Z=mnUugIK#-geI!KulrMJCItsILtY`#^_ON(pFKP2P4f1CE4BTOPibm;k^JFm!YXS) zpuL`RW@p}$t$Fju?B3b-lw~`j*K@TuNL~yL`u*~?=G`BMA@<%WLbCV8vW$(n4OiP- z0=u>fPVxF=k)9Vek*UF^SLcOp#aE?TCWVk0cdmyN&Ar|8Z~^nT%@Rxw+3Tmws1$no zY0c$p?=2esAeUai5*%21F*2;!5F*Cva5>h0j^u`zS$`!srXEPGjXCMNXPfns4#Stn z#PUkF+<9`XZ)?ZzysHmtHisY0jpG#t&CN~z&51p{$z~oQiIa3<F4 z=Av}F-JB0gd1tNuxlA%jrfz~?!I@|cj-{QU9cR8K{I;2G9o{oF!DO94<{1;F1RnXY z2hD4)_%m>~FNt$qHmUPMx$}`ftG@A8hxYsuowPdV-%@vmE&4yEO<#F#(L1-TJAQhv hG@dE?+Ul}}-`0S>jY`Ve-rs_G%Ih>RG-JARP z?wva~fb}~CSjBFq6oz0pP*yVf`N-R|8{2PZ!}RDGJAt3vd$eP+quURHe7z(fPwNZ3 z<1*^yHMZjI@k0PMcg>#R&ceU#>vvs^+1YpT-)|Ob_mrs=4Up&x3jZuMHaq&@#n`THkzugJ)Hil z{;ZMnO5XBq#%ZA8;_v?M0oZeCsk>1%|G%|41XAy4|GJqW<sAG>vkfJz66-NH)aoQmQcZV9}TNJ8;=44b(6FMVL_B@|rS&Oc>3O zl!%{DOwe56Yz1)&lbz$lhTu5LxZ(wJ*a8wGOeBH2C(w^W1e7Q^u>vJHdimM)<)liI zG`j@evI=Dw+&!h7m@Y6?V*Q==FcZ2!K&vpbK%6vUgfOhXc|g)w7qSY0Y-R(5k_NG| zz8(Oj1%iPR2pGdE&15;or7Nf}kRWXuQH_2)GEi~|NtBZarN_jrQ&@yvy!$+wQP$sr zic|p>Y+zz|4#O-Q-wbJ>5gbhvH9yc|NkptdzdVAzgovr)ZLA_cUKVa3a5i9(4G}%L zAv9t@%PADmnS?=wB38oP5LcCZZwUzVr)g1rL7YsZRE5xE6m7LwNQ!yQ)GGQX z!!<@KhTBK9fqPU1QIyt&eWGW2Vmw9iP$G?TLrf*|k#LNA3VCXkA`>^ny(|-HQNU3C z{ps0}f;}4{QiykLxJc(AhL0NaN|k8mM)#S#cJGGr2CrNkyuwXPq7gInElj@wRKsl{ z;VY;dt73&}ZX`nntc6?G+XcHuMah4x6s^!G_oK~?Dr%%$_lXh@`qVJyE+O5)?_xhqb`U2%MWl5&en zT~||700i~QWK!76={onKM#>bxr{9AV0#-g?w2S~aNOdMVfkV^hQb*w_VxRr@VE!!b z8qCcpNY-#RFMlib|M?c(p8^EQi~t2A`egt)!um4cUI`YFFOcQ2f*soaJQ2QVT4b2? zs#F=E;A>U!gusBmEKpw0f9!}?*@#6`&c%dk45RH2*VNZM<(uFsUV^tO-t_H1|8T>4y7 z$_v%Fj=j$$Hp%L#xhtODJ#@eJVfcu+zlEVt%h`EjVQ2znTs(7(Cj3HQxq+^*lffje zT*q5+Nk3eu|9VS+@9x&j-+g$-9Q*v^h`Af1x_7VE^+d1WOy0{n6H`0;#?Z`F5%RJ# z5xdsL&6OOue=+QK$@Gz*$~sR9uWRo233pvXl965I^pwJ*=#@Q154w-tp0FGynLYRBOLd^XaX6<6}D(lF&(FZAZ_w#>iYbbfr)VY=T{8Tpxc*~FCmpsaYw-4Rur_1!0T z?fv;$$NAW)k9@wN=ay`paecGl+4d}JPS2q&Z?xOGAFY3$i^SI?f)?VWcVYd!kJi+z zcY00wC$jHs&9!$s{dN!YZ>zgm=J-Q%@agljuP;uk?drJvU~flbTVY$(#%F1(+~9UhGc?woQ!a&G{hXe`I7R`~Uy| literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/7fa4ff68ec04a99d.der b/tests/fixtures/diff_corpus/7fa4ff68ec04a99d.der new file mode 100644 index 0000000000000000000000000000000000000000..993b2eb3016820fa947fbc127807c06ea0226347 GIT binary patch literal 1559 zcmaKrc~BE)7{;?nSVFi-OiB@1MNm<~x8X`G1B6>t6aq-0MPm|75Qq~N0r7w!!K0uR zsR(im&K(V3{p-QLy(ak0c0IO4S@kniXg)@Vgw(h z0drkKp;1G`sYtL`o-9sB>A*r))TbHF6%b!3mX0n03w2d0Z9%w1j70GL!@|QN_~A&1 z7)zDOx1%huO4ldR>}GrNlw=GErZLJvQNRYX zS2WYu8K7)u{C4HKa-%`kk*}-<`k+?8a?Ywnpj<4##ByZ4AW0|@C*vV%22AEG>+=N( zaWc6G{SGkZ?3(h$LQJ|%hOCn&N-?nrbqB6DA49%LcdW z6SN=8?^qO>I(p@;hl~`a%zax~ubP9>rx@8Gbr0Eg1()^JS2lXpu50&pc*UI=$m+3S zuz7!F+F!BVm5VGInn8*^3jJicDezHm6}y08U`Da^?0b@HD*HK<9#55aZh+*{-u{Ah ziR6kZE5+W1;wx`D%R1fD%8My3Q<1Ew4g(Wd6m!jeaIAc}q$aZdSk+JER}U0Xtd?4z z1v%$0)U(QVmA2jRS@?|jZscij-{?^jRhu{Coh-*ESTs=$D*jQks+L9PAC79SYqw2eBy@dpti%t_dh8GsH`R>F{;o<0ehkBGoZHvu8zJOdae;~@c1V0c@W5k!Q* zMqSf}yaogU{|es?7<$nS)Ve*b|6~1N?|HnxRd^2pd z;JQgr`-bk8!1bR`rCCCQH0w;+bo&lfSTX8cyeVu7E!$o_8h++ro;OqS+1zY*Ur)nW zgtGhXMYms-OxX8r=ikj z^WMjY1_OecB-Gx(1<52kA%Ka{{^jXvz1*1E%gt4Y;fY%RoF^9#KR@W6zruJL zW{z{bO?-we;HiucUFMRle-9OXq&-@&;c$a=z--Gkk;4djdH7P*YI=$NkYR?FnBR1= zjdS8hxA7XRQrQ&yx=S?DTTwNcRMz5X-lQy|L^<9*XD50eo1)^ho!=K7kUbLG(=>L{ z+;#i!@flr&`j?+uMU8d0ZjPNB3LU8~c$$7oljVJz8l$oBXxrj2bmGQApPu8r0Yw(l zt3~&2_8ZIB1|~f(=wL4w=K2oTvpuQ(Yt^Rh;S%QcNi9(?R&dQrE~+djG2}7e0*NyVGY1(O8_0?C z8krdw8<`p!nOK?_MS;0yM#f;SVU&Rc%p7K6-lEdt65X7{ymY<%qI82MMkQnyFtRc* zH!<=v0L8hOniv@wX3U6q{pR$x^2QeheC=IfBj zi(2}jfhUc3$s&cVJ2_<-O#5v*mv{B}h5wb)khDs^5XSuSs}ZlcgZ-=tg~r#fZ>e+i z*uuK1?|Z6Qdwbfc|GCZ1?>*M^GBGnUFfMLl(lcmc(lX!!hPf<1BjbM-7G@^a1qQ+( zzA6hSGB~u^7+G1_nHdduK!U;`MN9?^2C^U?ABz}^NW{rat1_M~^*D1w`-Rh{{q_4? z(=rU?LDI@B5(Z)oBF|UP=hrb_5PUh#zv9HE7C!51lZ*|FVJ0&&vZx!V8Ysi~225?@ z86_nJR{HuyhQ@lCdO4}Uv;m4#eFJTfse&vj21-q+@!cd?US6(;ngqa}Pyl(tfP;-K zy^)8JiP1n0WP&`z#s!KC`F$m%gjulR~9XbyQtN!i{7; zLk^$+zuVTDGHXAGs53oq)V&U7gFb@}d9W0A#M^QLWD&m&=L6p(dDq8v>ui=NExx!cRBrCmWf`{~?J(cp zz0+Rbg6YKGwZD4x_1lkHJ(G6%Vq#R=aKTCANvfg3GpoFveV?YvFdY46yz^thoaieD z1Klh(s=kl%uU}NL>W_V%v|OOlyzRQ?^S>l)*f8fK>z393=hZoUV6)71d&)h*{OW5# zHZ$=D09~ce$YU4VydLT7kGFS~B|4um{=7+bW~_MeGb`TsgtmIUO;6Wp OovKRuI`iXec>w^v5!N&S literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/83624fd338c8d9b0.der b/tests/fixtures/diff_corpus/83624fd338c8d9b0.der new file mode 100644 index 0000000000000000000000000000000000000000..b187e36c701953011c794aac8f903d780efe9390 GIT binary patch literal 1114 zcmXqLVhJ;7VzOJn%*4pVB#=CHa`GX+f~LYqiT#JKzYR0sW#iOp^Jx3d%gD&h%3$Dc z$Zf#M#vIDRCd?EXY^Z6V4&rbLD|qIWq!#6+mM8?LCYKgvmQ*SPr52|q7A0pWxEJM@ z78r^e2!oU|3-fpe2e~T*<>!|uL>L;#iSrtn7#JHG8-PI+kZTU)8W|azTAEr$85p5j zC}to6v5?m%wM4yG!8I?rsIs60rifXX$<@N3%s_yR9q8(FwoHs{tlEt%3`)#N3@jH@ zKgHeg-onE)=hCKM?_TDtwpwSg@XHlTBjck^SI!vom>gGBm{1V&B1vDSaI@3(%6)FU z2XDJC6kc%rexq8l^_5=PZ?;XwM&G_@KmR(-YKw5XM0K`ZugA?-_uj6{wQKiT-1x(w z@v8wJ(1Ei2jEw(TSb$;LW*`gVtFnMx&Y{i5$jZvj%mimK8VG@;g+apI&}Zx;{8|WEEI= z47dz9*x1q=c^H`(!LchV&!S);XCSjcYJmi@?qp=$sL6!63FwP-VGlQjOKUh=7hS9= zJ;mevY{fz$vl5ryx4v_}9(o`wUsjOtN%Q+oF~K#n=GZ1*Jn&Gl=hFjC=EWHYKiK77 zT(>zj=jri*8>pc-tA+v3=9W6OoeJfA6*Cbvyk$^h{yzqSTvCt6OJ%D|_^<*ynb09?5LiFNZ;|EMPrf!@Ovi60*y7q#WYTf(7b0@5`k1*bsz3r3o zru?sR@$3G7U3IK_Ybu}e()Cl9Jxh{!8Mbao|JPRvB`=($wAD|(5mRRguv)bBHAmaY z=@XBgKUn2s_3V$3;?fkh-Tj^BXRLZ9+dD+F&%aV*mR8-f`Y_|(_x9)AIJ-3zo_@By zyMdY1ZV5OS>Z$ M>@S{ISGw2}0BRni`Tzg` literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/873f0ba80e3ac222.der b/tests/fixtures/diff_corpus/873f0ba80e3ac222.der new file mode 100644 index 0000000000000000000000000000000000000000..bd4e397246f6af680488936281c31a5eea1bd381 GIT binary patch literal 867 zcmXqLVvaXxVtTfKnTe5!Nxh17S@Hy1e7M_WF{*F zrxul^7AZKEmSm*nm1HI-mSpDVDFo%`mngWJm>I~4^BNf%7#kTGfI*Zvudx}BYiML_ zYH4a2WspFu1I!IfAr8ZUx6Ylfp)Wiaw1iKrz7r9j{bkW#VV&75N25z>H*cy^n}ZS0)&s@QBF#m~e!3r(`Tjt9zrFN(#8}Nao`577iv#}?NMSOF z=9(kI+`dlr*Z*08t&4e-yd2KB2Rbe*W{}|NIQebb#e{Ajj-JY6Z#4q*L literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/87a4d12db9b57d68.der b/tests/fixtures/diff_corpus/87a4d12db9b57d68.der new file mode 100644 index 0000000000000000000000000000000000000000..d41db31174b0aadec8a3813bd12f8b19b467c111 GIT binary patch literal 2040 zcmZ8ieOwcD7{9w6V}K0V#u5=31f){qH$oVQ)p!wC%fS#|Rte*nV>NcP8)GPjT!^SB zkeCLdD=bOTkieJHNcr$$K)#hlh?FlWqLREQil6Lmf-e5pXZL)c=lMS0dw$=$1El>h zAeome3LJ0zo07P$kH)nHU=>a-kGav@9-DhU-&t{RCkD444gGda6kf zsgN#~CM=}><&x+qNy1|3qF6zcToET%NES=wi$pxYd8*MVD=SNwl7a~_JROj*kL_*4 zb`XPvT_6f~L`WEhPREq68_E=oUCS2q?n!dI;PE)RdYf|fjXh_2Ymu@>LtE9@RyQ}Z zc|gONuMQtL7`DSa_$V#MZ)fvR`+)EE!mf{BQ+Qe+r+2tAXVD`|{>HgShkZA-toX-V zS#|&IVGm{4AiI6U?(4HXt5q$dr+tpQ)&2QPv;wRNrvG55j; zZQ|&FsU94!@awJEn9Gmom86nY)WzjL4YGF}WEmq2&osId~gLm=2UQ6t)DxJP-_mCMXe@?@v<_ zlrq{ElVlmx891Re;)d)%z+y5rc~hu|2E0!5dZA(v0)j0;7EmA@SA`jLlNlz9qV7mX zxhsdJa79U&paPt<^(5*1+@QkD4@k~AuHKHY`81+fnN^kHv8h-TQ{qX21HO z>m@t+Wj>8FAQRLse&DttC;#*4q<2pCjGfjGmM`y&b=2Cet8M2s@s`~fAM0%$8w+9( z0e@HbZl0U~y?=kmPF7d$4DUAoygf3xVasRNHhxC_p+dZ&R(sK1Sxj8qp_gaQ8wo#E zGNW99?$-ysq9SoAW$Ck8`^zl#l@c%2(hd-&n>bKd!H1)x6wH7+O}zCzp4wbVMknR z-aK~~MC7N`oq)Smo{uM?EOzs)3;Si?pcS_k1y|k<==fEd!eZSzz5A~IoK#H*}4Ar*muk1LoPm;!|{spRJSch3wLc^mBfgz z*xjoC;?*Rqv|pTbB60bGH)jna`GelE`&N)3OjcNLn9mJ4oUM3V*<)6Ae>MGTeCfZk jJ;#b3aTp-&aNduD_tZ6a>NYSuSZxcXtNGG>^PBzy0(<7B literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/87c71553445eb3c3.der b/tests/fixtures/diff_corpus/87c71553445eb3c3.der new file mode 100644 index 0000000000000000000000000000000000000000..b6ad2578f75fe725c0145c62803876e4ba8de4b7 GIT binary patch literal 774 zcmXqLVrDXEV(MJL%*4pV#LM_DUTLb6C+knfi7W0HaItY{wRxPgWnpGE2ryJJP-0^a zW#JZzG zAeZ@vDFg@mfZS$eU}#}bW+1@E4)zffBO9xBBMXBPa}oo~;e~>|smc$hJ>0!|*#+iU zf8V!~{C^4`x2TG)+{$uJPWN%$!5;=$(bAJ_4!9`WZqmO$m%YpEQo}UnG?RyyXU1Nw zXPmw8#IN7E?V)LTp-=yZK4M##QM!B6A;(k?}tZ z3oy9b41_>@VGy6gfDK47F)|p)g2eb(#8^Zks{2Z{{v|f7n&7~=?%E4>w>*0f19_0N zGK++PSc8b|Gfj`#uh>s1x_GJG%e!}}`NI5213NYjZ8k<$R(3{479(K5>cRL1jBQF8 zB_#z``uh3F#RW!s>EKu_2F9XZa(=FU5ik~uObyIH7Ra;c8|WHnFVI||-lhuGlw6d9 zO^a@ksUA?uz#XJdfyKeV&cKF^1;}JDFflNKSMI3m0Q^=ahK>yYZb_f2pL(jsU9!{$IEIT!S+nNV&u1G1y*04A+r*d%JP{_Lg24D~PoiSh4U literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/8a709990bf0363e3.der b/tests/fixtures/diff_corpus/8a709990bf0363e3.der new file mode 100644 index 0000000000000000000000000000000000000000..78bff57893c911e69d0de497e9b972a738d6eaaf GIT binary patch literal 1515 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 Iwa$kD06C_A-v9sr literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/8eb2f17d668941c3.der b/tests/fixtures/diff_corpus/8eb2f17d668941c3.der new file mode 100644 index 0000000000000000000000000000000000000000..4161ee7716da295982ad295f280b92f3e74e8e9f GIT binary patch literal 1599 zcmXqLVzV}AVo_ee%*4pVB*-v3VDU}wC&!JMgj~JPWE58z@Un4gwRyCC=VfH%VP!Cg zH{>?pWMd9xVH0L@cQTYP5Cd_zg!zM0lS?ww^A&tDb2Cd)Qw+@vOh7Ws!n!b-fYPL# z%w&b&)S|M~A_d3Nl8n^6lFa19lFa-(g`oWW5``cWGXptsUL#WjQ$rI2Fo+W81+q*H zjg5>=Eln+>3=)WSfVqJw!~uFlIl$Rb!9NV>0t164#zn}!W@KexZer|b0E%}pH8D0a zbOy<-a%Bjp6b;>}9(sSno8X!EBAkAf>sr1}Y>lsKTegpBH}l>wnbIGw#jI6Ps=1}y z6NvmDbXQaa^w!EfbzPq8T%E{I9oI*)M|XF$-rn+%$Q8{i~*psu%a2OSO`-q9m3ty0R$aV3O+3dnPkf zxYaUGE86;4hR$2t{W~=*<=*N$i!L9It}#)iL#*8=hTwv~h!Sv*uS%yRfONWfnQjh`xK#=wEMFA-DR} zmJRPuzIm``J?{_sHpLT%;sf99NlU&ZzQp>*or76DnYO}f1AhMEWMXDyU|ih9m=BDR zYy){3xhgh67eEIbBWY#iEb zjI6Be%#0901(2Wt2OC>@BM&hD7=(aS%Cqwa9`ls=A2PdQedU8pInrqR}9G_ zddc~@`p`59$rF$)ftn*g83LFe^ne--8e3rYGBUDM7?c_K!}tbFNH&y^XG4K`vVk(( zK*lz4sFU-PiwlrVM9vhzqJX&x7&HNTwK9r=`K6)R%=^q2Gl?!J+x&_*WZ^sJxW&El z%1_Hm?JD0e#LbxZ->l}-l-ZBtr`@QuQVK}2GYXsQGll!%;>M=HZLMF#E$#Q&oR2d; zzu??OW3J}dm2Wd!8LS>yt(&%O_HJVlQ}!z>%{-@_QIe899Xs{YdMR1&hug|t__san za5O)WQZgyYqSfg1OqrO)vtOmkGF)w0cTl#nspU{@>GQ zb`>(h-PYSatv)^Hi@mw>&1pX;o&7qk>2v0+qc_*bW~K*jw_$Zyx+uH0qsLb-hvhzh z^zyfgqJejQx>qsuMT$N-?EL(f%q{2SytbGC!^cM!oPYnX$NZRr(ZlrX9G?s~-O6up zzw%G}Qs`!zPjTBkUi_PylEL17v~KG>U*5!>CpKL(f1jzba!GP`rt`sN`FV5aZg>7I z-|@C@(gSr{+0(j9&gK7RTKi{G>gnrK?gzH-JpQ2Ye%sZ(jbe{0mwR^0$*p}V?~{`I zQM#~Y#Wf-0xfWjn&P|^dWss6J*}M0Cl(fNogBd&*O*2?#=gZ~%hz?^vH1oj5`FxpT-vvuHUs+m(^BHS?1h+FL;{t us{7lbMRSZ#RZUU)AfaVsW6Q&#D9^IsO^tDarS;<4wI_D_K1h$eWd{JH19D~n literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/8ecde6884f3d87b1.der b/tests/fixtures/diff_corpus/8ecde6884f3d87b1.der new file mode 100644 index 0000000000000000000000000000000000000000..86b7dcd0b416fcbfd1636f5c019403ede4ddb413 GIT binary patch literal 837 zcmXqLVs@5qwPB{BO^B} zgMpDG!O>~FbfMn1Qdeu^Gg(*9Tf}> z*0AF)}i2s%R)Y zRdMxk;d=LmfRft08`nF^zRb+{9-)}ca~YmCeB;(ugPrbQ?IZmt`C3y-6Hm@&Zwbc1Q{rc}h`cPmw8PecTBE+ObT9 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/8f14bd119660d75f.der b/tests/fixtures/diff_corpus/8f14bd119660d75f.der new file mode 100644 index 0000000000000000000000000000000000000000..5c94775da27574edaaeff700ad9cf9a2f019f7d1 GIT binary patch literal 2454 zcmb7F3s_S}7QXi;1i}Ia1IWWbz=9wUCW(sR1CY1njVS6OVu&#Wl9&VnE{{zrK~NBp zwj!ViSV}9d2rRNZMdeXNP_`f~pgb0&EftG`rn|B?5yahf>(~2zGk4B8bMAj;?mzzk zDLw~CRgV^75Ewy-bWE_#4ffQ9?D3vH9SBlbKof^wC-aicYr?RG3IxKae*kzKCIpKk zA|b(4W3XCbs1tR4*$e?s$U7t=yYd8lo*+WR=5eVMV55j*h~~?2m!q))COIhBg}l=z znCvYS#5C>1|0n!nEoww$w+od3v=msX zMAaZ>WDJiR`N8U}z}h#}k%gj&7(R^>&112-l$fQZ@VL=zE|bFG#Q-U6_U=}|DiB5r zuYgd$IwFN(=$qM9(=W~ZpX3#X7&AH#sLnB1cP*;gEaj_ePNp(kQa zzZQwIJxzasjbC{ASpd$xkPwh?U?ws(39nUMUa?@Ut{D-6FnW7F@+-i}Z1b#MoYKo2 z=9Gr=hv?n*E_UTV%zkOaRW%&06x-$mX3w|Er(3>g>bA1JL#XUKyQhj!oT=aNRcNz@ zW8DZTpTuC6{j%{w9w!CA<7~|d?}#MA+(7H#aRa|g1d5;|Ai>H|-@|PEuhxX9M9WgY z$KI9~3R40Sc4b!HweKzC^GgW_0PndZXVUJAH zZ4Wh;?28l2CX;YVw1T=8flZXap1>U;`_lG{rJRqC zM_a{_hn@Sv$20vT>95RAz%PXXO|_5&#m%ah*QIh}gEJEya-kXXt#Lo|puXpyg*L5# z1R;78MJR4Z$*q+3s*1V|9M~!9=*pNeM?cp~Z62>auW*RXBXyr!#PwOmh3hjOta)9& ziwYWVt&hmazJi^c-{bZu&^OOO>tf)VYbb86!zF>#bY%YXFTdhz>aK~LA8Q#NpWokQ z1zT2IX5oWSX5*DVKx(h6jx_}OKpm%sI#g6tF~~b(7;IUdsEV}%wm9>pJhimOej~}` zj~-Gn8QkeAXNsu=E%mjfW5!e<;s+T?s!&pffkdy-9k_R9%)?tx|?`UTy z7K@jUmEu6z@i}ZBUuef*3PeJ?x8h(<14?;7->5*KJt`4szbF#u$W`G4bhNWYTfC6X z;!+p_5nA@*>HsV=T?FNEnLLJ&|6jlg7a*bMo}`7YsZYX2yf|z`skS*c~5-5j7F+dH6V@Wj7I}?~FaMob;f!?2WUgM8Aho}(T z9+paCub(%Z+JQUX@rG*VSUsS#1d2i4j^KAM74&|^ND&xH&x{;AB6_&-eAumhjvl$> zO?~k(^uK&u_xF}>@B1IyadKhog~KlskC-LAm2|gXZ9H2857mmuU)(n!4Q$EUe(zKI z&h7Y`8>7tw$XcK}Q`oSLw%e;WF=Nlx$*J=BZFt==jyd5}wpUZ(g#K(b_h7FRejDZd z70Fo3T)p&O*iSQ0YiF*Xdthwnv)aD*ht=MFPZRe9%Byz=H=M3N*iRE>xpKM!kXDOz zKQ(o3E_QZ$R!!$lIAd#%wRzkZ77Vg3-xxe#ifbR&XfsXa44I8K)s1RLrAKdb9EoA= z$_SUqx+@FESKQWdR?8w>EP8Cu82Sie!B88Dlf8 zL*i?_Wo2C5C%*fgilwt>L}5>#<=hkc-yRq&qE0MudNyRVHL5?Jh`Ey<5}a$1T{^8p zw;7r$Im)AJQPbqZ+C^y>1*A$`6L1ksrcNyk-IRZ~bLi#cBT#ZD)1oH-R1*KLb_B8i z!5w7nVT50B*UR3Zwzb{0SnUV_OmM7^2O2fNC$)|ho=Lp&$A!m#3$3K7_guf~=`k#y zYVOH>dd(@nN!G1a#kY{#SP)r0*7>uZ6J@Up`ib8kNH7e|GD#aYI-S!1o~shW)#ppj oamsS@wvTVvVI0M3J!LK(TS>3q`qyYtSh`3*BIDYK6Z`DH0Sb4OkN^Mx literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/8fac576439c9fd3e.der b/tests/fixtures/diff_corpus/8fac576439c9fd3e.der new file mode 100644 index 0000000000000000000000000000000000000000..0f9320247fd95e10d8271b2ca38a383c4c474f80 GIT binary patch literal 1167 zcmXqLV(B($VlH05%*4pVB*4@6;3RKWr)UQYo7^MEpX&{H**LY@JlekVGBR?rG8iNp zavN~6F^96S2{VNT8;TkTgE(BmJT95(na-(2B?_K-$%c{!;viveVS)1Ua=jFwR5DOX zFF8NgP{BYBB*iQ&4%gwHlb@8BqY#vzU!ve{WFRNbYiMj>VPI%zWB>$F;=D!{NL&LK zqMRUSAOo>S6x|+YN02Q|j7rFUV`ODuZerwT0E%-lH8CGRbwvXpI{-7Ay2N#=IqicbB6=dTZWNgT=7dMPl8^R4(r2JL6H?^!xU{EQcF z;-B6=fAfN~`g?TV>}bv~w3WZ=H9b+UC-~?FI;2!cGtQI zf2Mw~uHZC_E14{(`r-eTwd*pnkNxMrYWguW&%^V_j~(wjete7*EMIk>X)_ZuBLm~& zCdMdWh=dsk0mD*On33^63x@$4kYZwFFyI4;@q@%zfN7`Az=VxMn~jl`m7S51MZ-YN zKn2D(U~H4fC@Cqh($~*VE-pYx4+hmBQ{-7n42lfQ7nm+EZZm``O)knYK~bs?i%3X% z0VNlABR!xpOpV4wYP1DeuE1huU}9jz#sXwA7-$)2z}(HmC?*4POfk^m=;>JBIUv|T z7NnhzMT|v6B~M%TLe`|fK-EmfRkPfebv}1cFpvjHE3-%#h&71#DTOFVOnvq^?-~EE zUps}~vWfRwBBwuK-U6mSMh0%nmWe;7zh2e2zU5_<#iT!vaw7Meop4=l$SnAf?Z6tN z<58iSxBMNLzAn7C;qAQxKfaaDE!)}t&#|HEeC%U}W6?~{W>+Mgyt5-|R{7gbj_ck> z|2khyQdU305}8|QuN?jA=BMW>>iJ)$Hr$@3^CYDE{42?u^`SEZtN3&!Y%3yy_9SP7 zbKiB@9PX9y>_kC9bh}2@b!)#MSIPA%Y?C|$rMlOxkq`=B!+d(jm%XRd_?92M``GyX zflve9sgZtjn9bOJ9OxIBImcIP-ob~p*6WLwyY3R6rs*enL23ru7xO9NYZpuGvfIGO f15BW%3Z}JR1rnZo&q$JcBl%F}Q@h}rmmCcMc7L5A literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/90500fe49040b550.der b/tests/fixtures/diff_corpus/90500fe49040b550.der new file mode 100644 index 0000000000000000000000000000000000000000..f43b2b4060fc6cee738e15041799ce9e974e5480 GIT binary patch literal 1668 zcmY*Zc{Ege9KZL@%*7alF(jjrNK?jd43$@8EMs|E9wnSe4UNg#m_d}7B+H==DND*$ z980LsF+@!fN_(Aby%v*}_dJD!>fK4s^X@snKR(~(^ZkDB_jA5Lj3xjvexwM8zzCua z)wKn*4$P+GGc>Fv593~NfC{QaPj*iJq5{Lp1PJ&um4On9+m1&y5hpvQ4xq>ZQB%z$ zkSpZ#!f9^&P`)rQfVmb}$~`zu#=K{%Xio^AOY;a6L_Z1Fignj?iTXH!)W$w znu90JowWw&Ypa;A0p?5=`1$!@a4iNc%~_UKmR3H1!XyKYpVTB}I)fj|6A2jH@K7Lz zmo01-CO|kbtOj9rA|i%isK|)?s;cHu-mWo+AFZBml&GkcE5^K%lYM*dr#K&@*1e{)6Isu)m3RBhHBgRBF6(|^v|

ELt8Mh;aO*#BUKe?+SfrQG_aCH|gTN2*rmu4@P-s7743c~r2(F6M~$PH9HRQB#lb2dI`$ zUtGGj<6j*P{Oa^2ys^#+1P4QKsTlbN#K>2$6#IizLp-*>1aeTj21Q4;ws-Q_1@j%H zoIiYf6jlJLJU|ss!IMpk60&FRzV`{2?w2mtEdMh6Zm}OwmAlno_y-=3LkLBHB*&U^ zi~tB|%8?PC1c<2ODikIV6mT$bksE1vJ75beP-V=Fhc7WP(J25UU^pL#z!dEup)exy zH#4)yIZ4U4Yi7UI11O>>FOdWzFi1?AkB4D=42S|-<}pB$?dADcI#(cMETs6Q!#Nqw zsqp_Iv}ZebVhUtniT^bO;N+hTVT9q_$cTlEz(78cI$js(h!;vxth7*-87T4RtWOUh zcmywo4`J^B91V#ef4BVF*u&Ag;K%4}mdfqC56XInrn=mhOiyb^uRopM1wrtcZI@Xz z2*Tqa4sbwd0V4uRn6>p%2gY+~7+rYpa^udFNku(M83j?nCMUYG4FO;PdO zVc@zq7*<(2l`=l-exlmKrf2`a%nENP9?I-?Qm!7__qhbED~nBP;(TA)(UxOlBrDkb z266t}F)~-y@;synK?De?q+lzCoPZ-jL-qS=BZR~dyI_gC>D$z`Dg`{PeT?obqGGYG%>#rtQeebXkR`SV9-hvX( z+eIUTCtRaO6s`M?t%nuYnUFX9?fq-))VcbQu`uyheT_j9&B3VM=oeKW4>h;!jeZA4~gq$f|8mN1g7GaTjODzTccKCQ@$$q`~;-Ct3QclvB zqlo_|Hy``Vv$n^pjb2$!`1eMi?IOzVyoTdfwwtn1lBuf|C4X_;F=@TAELUc4`(WTg zYLen7*CYec^3+ZL==`u+{&l?j+l7l>YhPsaC{F7ou$m8tNcD{`5#j=~>D)=iliufy zix#{X<4Fzm5h`>%Z0vemLHRo0ICnRRU}(3FOHggcW}_RaHKTngZYL)Eo|iLc%UHg~ z&LgVqwYfPYqJqZ9Yg5g;le2G%B?Hsvk_K(t%VVYmk=M*Sx;JR+<-e)$kkZW3Po7QjVvm1pczBqxf3SYA fqVmJz<|k^49(`hsO+)=nqA01=B6An($F~0hVSarW literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/92f351bf3d54164d.der b/tests/fixtures/diff_corpus/92f351bf3d54164d.der new file mode 100644 index 0000000000000000000000000000000000000000..1e9527416290d8f24c4f79114e6201446040f8c0 GIT binary patch literal 1689 zcmY+Edpy%?9LM+DW)sbA%cWd?OLCdZvoVo$$}mH@Y*RR0n8`Ah+3M`X>)4cZQgm_4 z2}Kk+T@YO;q9Z9sDUwPFIUSd%<5G^ZarCO^pXd2}zt8vbdS2i617w(VAVbIJqmU31 zsfl>>%Z<91)Leh_%2i6mr0ps%p1;Fqu>xG&2o7fl!_g80z3CWME67fVmZ< zgR^x2WU>Rrmg-1#36Gq5)&<)0<<*&7j)=EI0DJSIcp`2X*&f)<7q(jP_=-dM zJPyp{3U_mbb0xXaqJ<9p1p;|vcJ@GqT>kCzKuQRd45^Ng+f|S(gBr!WlCmy7gaRjDDmt=-iWmVX2IsF&q1*862jk<^P-k>bj#oR4qkq`EZ zFY!fTYlP1-El!k4wzoz0xCaQCmc|(tD|>dYCFvXKddHMdQ_3cz-#K|c>pOvbdT<}U zB&x~Bw#7GN)iK-dZOH?9tY;phS+u9&GkH1=v_{BJwWHTqovX!X&4PxLtJgMW-u3bAFgyUZRrx^42OX<#|Ou`QX;w1 z*w=Q|@?M9bNit}0`!{r|Z3{)>a^Tg@mH@qV8y=S2n5x{(IPt-B^61&bK1?}BEup(d zi=9y96kg$Yz^ZBV&o;u~Tigbv>^(y}2T%`UjAgCOqiM0@1j zt}{!n7}RerzWgDeYA?e(un%5xZXeQo+TjH6Bm6tpyUPPzGMYSSP5CSTYK`@X(`oKs& zV+O`(`4}b;n37vTB_z*QR>JGfoZF|yuAsv20|UhcHMBOG5D>dLEPd+O(o0$v=upY& zbxV?o44|fPt3%K%8ZBov38*VHUZIr$O1{mcM1rtIf5vguSm*?{0_*k zJX*_x0Ck}t1fk7=DKJ^k07}yPKe>PtyX*Vx-%=$1WaJbtp`mA`Fx%s-ujt-D#liu|z1ida@%LE7#?&1i^jd?sx`feeu4YT6&=+ zIT{yP{8BS#>tJHX(sJ!|>fu+11-OA9KU?~qC*_Z{-Sfk$g+FX=qLyC`&!sI>$EUJ3 z&Fa)wJ*&*fYQEv1oi_72SK~u>hwiPl=Ug~8&!cwtUcf2^rZef}|UM}43B``UZfv)OFw z6iRC(__(y!Npj7m99wqD^wY&@>Rse>f2e+IZ1VmDlLPJ(q5o1MyzIOpB@L~H=Lsp@ z>2UD@ugV6~Low@wXU$Tj7qveZyuF^1R@&%X(&}2EdCh)$B$Gs}bFFEf&~xQGSv$tO zq0qnRdN>=cfHl%mg9jqp#}6Jay~E5K(`e@l_b+?gKe~E%P4)O}DE(E}#k);D=7C(j k>DqpVPtMJ3H+OupF(Uc`PIPPF%IV$ilCk1E9XAr=KYAdii~s-t literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/9588ef74199e45ac.der b/tests/fixtures/diff_corpus/9588ef74199e45ac.der new file mode 100644 index 0000000000000000000000000000000000000000..63764422b3cb9a839410370fe9ab6e1dfd8488f8 GIT binary patch literal 1344 zcmXqLVzn`7Vo_PZ%*4pVB*2*7FaN$nDfGsM<$T%In-mOq**LY@JlekVGBR?rG8iNp zavN~6F^96S2{VNT8;TkTgE(BmJT95(na-(2B?_K-$%c{!;viveVS)1Ua=jFwR5DOX zFF8NgP{BYBB*iQ&4%gwHlb@8BqY#vzU!ve{WFRNbYh++xZeV0+WMX7$5GBrQjLbD~ zA<7AI1~L$PMB(S)6W%Kd-Ua# z{Voe0{(9&ZXOJW(J2#ixGQ_QQcd7Ig)9;phxfZLg-t;u^0?Y5^<=ON1Et`I^tC)-s6|6eZ>0x$y;%Sb$rL?I?OvOq(9rKOWmi? z_}iX&f=AhRvb=v+pqc&W(8|}AQ!BqNIc=dksoXO{dZN+epJE3W{eRcZa($bFOXM2; zlzJv+Mh3>kO-$SdO-$?tvcRyE6J*vN{_TjMr;Q1AZcY5 z2?MbP5kI961&OK89_Kyd|MhF9&|5a~eoF&BkOF>2#{VoVz;xCIF;A66%s_;VLz|6} zm6e^D5zb;V5CSO|1}Wz-U;|Q2j0^@QFnLBs77YV60~HwGfU!*?qokz3N?$)exwrr& zxfxW0Op<3QF(@)HUtqexxXlo%G`T3p7)7Z*ES*4-BPcbx8|eX+VQMrXQe)#eknIXA zjmHcc4;wTdU}FLD7z`S>8)(2B&%`Ju19DL@(DmqfMc+9f*r2fsu9V53j;F>kuTr73 zI8`A(O`#+svseL?=Q7hWlM_o)6_WGwic2y}N=s6U6%vz^QwvHG^OB)TQx$?zb1F0Q z(iH*{i%KdL9Mg+ZQ*%@EN)&SPL7Gz(5=#_tx}>NeQMWV^Il}-817L<>WC$|iI#k-b z&-%9J+~)iP6>Yz7I;rky(Ule1)bwGw;Y!b&?EJezc?>;g*Z*ffsPjAS^0hwqto!XQ z*7|Py?%p|QwD`vKNj}~!3l*3qn2Ft;m-_j&UuVqs$sas3pWg|qVH0<=ZhP&+XLru| zl2)^L_t$?B8+^_4Bmb2h^4|D=#m2SoSd)aloGA4-{86X+w_-ucQRd9Z&L48IZ_fy6 z@9A5}9>1W$Z3dsu@}`HzR+hK6|NrNDzQ*@MLR+ZZ(!Ar6>3)t8Gru2-zkhmB-KEHd zbDNjMOm1;+DY~{T=i5}XN)9ixX$Ehk4O%58|6XXP^^#vhME6hE?z)`r{$ zoNUaYENsF|p}~f71~MQHm$0aNetvpRszOLnX>o}{aB5LmW^!t=ArDY9P@Y+sIo#FI zKu(<3$jrdl(8$oz)Y8x_3d}V#G%zwSwKOn|G7vQsHV}lU=gLh@$t=y)OU}ikJ)-3}it(J{B<+k=%3{WziKEOZa^cE7t8x zJKip7xz#`(B(2OMVIbBZGNF8uY5I}>E6%KZR(-Rfvii*ApB4sjFq0V>S%M4#4AfzK z1I9M#jFOT9D}DWZy@Kpay>wvE>KE&mryA;ec_kaD!WA;LNkSE7A`}9Z=_MDH7$}1r zAjl$PAk~Bvm`yxddPqSF_NM~Kp9UOkZ0U_Wj7*FMW+0vNEcynz2HFcW7pS+XLTyP# zu|>bcBr?7v*d#yD(h+E5jzJS63mX?Ov{)FgRWh+Ku{3^VY5c_SfuW2cMes<@5}`NC zJ7pFz=`rkC?XSUpMb1=x^{anZnNQZ9{lLJ$IOp+=owFDiSeO~y4P2QNgqO_`T)8G- zdVS$eoh$z)?k=@Rycqgc-v7C!?fOr*qpg?}88)Z5d^lOAoW1eY+w_j8gSTVP6|;)$ zijQP*&$3+pHe@@{UcT@tdp@sQX=g00Gn0Mu_X+oBAF?epQoXCVQguz@Q9nhfz4dT= z6&co^RN3;RHUH;LePtivtIiMqsMHtnczyr%gyYRJq1+W-ObQ`^t2f8)IFr37#U;ph z%9&SDzan}p_U&S=uYWbO@dk4ia(-dS#`JCEPbjh(^g91!%^GC%Or@ng^K zxGld?;9CD#%k{S>lLE_$ec$Fhm8w5kq4r(7eBVqB>s-d31E01||5fd?pWMd9xVH0Kw4K~y?PzQ0igcUsVN>YpRQcDzqQMi96N{2F6x@sQ zOA8D|4TM2TnT2^ggM-`^g7WiA6e0`_)UeSUJ_S;M+V-u>HW)=goaf7yL{%}fvF;1?F_{JHX*^)7mb z_cWAjyQP1@qPLp4KvBB%lYz~z{&jb6C9i%h=6|S9(7WzD_ly5q%k{o&s`h%|Bc#ex z(95j3;9;=J8{wPpB=-w!_Uf_kT$~tqZ%sS8l;RAn=gy-c5l%vESRjulRoaDHHpQelw1#&mWmj<25Ut_nWV1qwMTG%s)L@ zZ#3Rz-J*5P@#PxEvZ-ABH|}5EDDklY(M=kbokat@+bL(=ez`Qo=d9_8$g;*;h-`WLMh;lRc_g>Iv-DFqo zCF5PpD)i^rs|NwXHO`YuHlHea-Y3t;=GdnK4#`;nE(6$dNYTB&bR(NQ2+$oz?wqHJLsjX!HYm3h*_fBZ@a%uek ze*2NA(-ox)>ah}I#svAgPldH?sMd^L9VXJTe#U|j5E;9$T9Os}&1 zjEw(TSb({M&43@o7Y6ZJ4VZzHfhFz=l^iUlGsD^9O_ z?o;@C)1`#9mMgeli7SS+ehlD?e0}ag-X~KPhVT7{&D4o6YKug*3J5*#Pa(8&H7gpwUsuC^Ywq~GKr43@rUtb$j%*V zXSzC!JAHIpY?|)Bn-;WsJ~s2)HigcR z-KW{sqcnToqipMNtEK|qJDkTmPjj*R=DdjQJNf?H>f^h&YWulf^SYpR=4sI>j;y6q zAB!&hzU1vmo%p4{|F6+t(%W~vdiUeP>Iq_(+2h=TYs}f5dM+QCHs|Whty&MJN;P<_ z^RZ+cWl3o${YKsGG(t*4WP8`@Gk9!gJ;MzXRq} z=D1zmBD#56Ufpb-X;wRebnUN2Km5&csO6u^ip8C`)?_`D(Av1dIWhXO{2lAwvQN4% zdQ0z%8|T;t|E@mm82|syq6>)@52x)|6WeWmz4WT_ftiBq<~klMDs9=vq@}}Tx=X#Z64=rS(up&5)8Qw zIN6v(S=fY`+?@<148%YjE@A%Q)Z~)P^n3-M%-qb9)D%N=15=O;v#=gaCZIGaCo@?g zIJKxOwMfCSv?L=nuOu@$u_QA;Pr=zy!6i(=)!58HPMp`s%)r>d%m56cz+9k!k+G?z zsb!RbsG+cdAjD{{^vsfs(j>j){9J=b17S9Hurrw$q3&g7WM_6_V6pY8{ju-2?US5h zoukap#b1}X&2v3=HD{BfmeGk>Pb*(XdHAxPbkg0FoLzOO^oXSA%PMta_U{~Osq=+u zEcC*n7B?}K8#FN$8^{A~l~rZ|`nf?weC7(4gZWQ%3s!Im*fL*!DYZi_*+3SgfR9Ct zMTA4R=_1R{T88XJ8yl6Hj~~C|-*d-+4|w)93GMkYps#uk`*Mn;wjgE9ku7~g=Y%^9R0s6Dx;L=V{rXxu@f z4ia;y5$Eja5(bPlJ)lN}gA5qkzzzZ$n4er+fNY{c6C*Pl7uX}$Dw$ZASQ@{uG=5}w z&rrs|7d~ar=XERXjHPvEvTy!A;r{GHwuMHjcNJHvu1P%Vr^vv-IOjmq&Q%NyEX)k< z2ChsB_21>U8=V!Yos-%)S6q?ZBF{em`_cCub6;MOTzCI)?~gHexG#v)gEOprANDS61l3ZH~u=3IrrJ? zb|wYgnOE&E_+8|SJa1`ts`#R|g7qPe*}M$v@4hliO0~6!H&6vdtRRb=flL!pDsJK{ zFE7_aO4UeNkJ+68WVgf4b|9)BhevWC6%-*Rw-1EPiHu>LXu1Rc+vfIedq!6(r j$na5sMBju@PREyQc4HDfRqU?rt)FTyKJ)p*xGx(3Wm`vL literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/973a41276ffd01e0.der b/tests/fixtures/diff_corpus/973a41276ffd01e0.der new file mode 100644 index 0000000000000000000000000000000000000000..885c45a4c350ea8fd2c4fa2ee2bf312e3cf7a6f9 GIT binary patch literal 1236 zcmXqLV!2?@#JpnxGZP~d6C=9;FB_*;n@8JsUPeZ4RtAH{WoRXN6YA9tO0TSdA7Ie>dNlZzp)Jx9K z)lu-wOV%?qG|&ghF$-(B=PQ6E6@v2fOB9?_i%K%nGLsWaQWYFaOEU6{GD|8IbQRo< z4CKUlf!a(Bj19~UfFMeo*BF^=(71)_uGBTq2Dy@3SS6#Rq`*pFKN;w}V!ibI6qtAP zi&6{ni!)2|iz@XEjSUPTp3;JQDmXQ{v?vvyx0)E0ki(mim4Ug5k)Oe!iIIz`iII_E z=Y!J%SJs#9y)rrJwL!{`Mvk1rEH{(H`}F_YrJnCUm-NggNamK9t9R(?itOa(qzUKv zGjnF1kP_+kIpb>ls^iYQ#p|{IH6+fh3_o&S?~A>GO|1K$Sz>z*n@@8L>=X13`Ju7z zLeIVr1{QLkH>mwcb5G8_SE%ORZ{Z$%(M@*C<2wwmT-WT~Q+RV#KEv(XXAP_>{o{Sy zf5^U%K6GSmg7wBt&J%AeG4^}4`I@@_TGg(l$G%*>uxwokYf5;>R+rE#x>{CtKX&YW zX~e}eCFO{iRlrBDb1}^xI>7 z*;7yc&cRDTznPdB85kEgF-ieLM9hF67{Ic^jEw(TSPhtg6gVZy@`D6efT@?wKo+Ec zk420{#Np6h^}aJE%?68%um8^3{9%hu!#M+akhC(3gn?Lth}EoCc7b$_ZSTMBWp&I` zI5_1N|2+c}HV$nzMpjmKMn)D512qE`7~g=gO#+$<@{@}TP!fTGDacrP7Ci$U1FZ!b z3)I@+38p9qMP+(Q5iptSrW@%c7v&hZfwU>G*c;dyShKMJnGC?xZ3wfGiBU`yWMeU* zgom7dfcX`eei#`z>T)IvoUqt2ZRG^-{|n>&S>ET^&FNq}8PLD1_UJ96syQ>U8BTzO1v{E!bN&@rz#@@ z|JftbNw)*HT;>ndn*C)@@t?ADW=qcAyrJ`I-R!Sgn5qymJw~$+d2Im+{+;x@Bf+cUWG}|DGr>dZ#`A ao!c~xwF}j1xj2{Wu`KT0^J5Q-0V4oR|FP8o literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/98c2692704108e8b.der b/tests/fixtures/diff_corpus/98c2692704108e8b.der new file mode 100644 index 0000000000000000000000000000000000000000..0c2a428b37dd453f1f03e09e3e5726e6c70d0db5 GIT binary patch literal 921 zcmXqLVxDTy#4NLbnTe5!Nr<(3Mat{T7x<3u+x2rtBD;S>1m4q2P7yAQp9AyU?2I zB4HrbAkxn(yy%-=-Nk?$V+o7c$UfzwiKh&VU?wv%vZxuT7%0K`225>Y86_nJR{Hv> z7J8X_IjO+l)XOhQ*Ei4v87j!4WT4Q*uce0)N=;~i2KIph$Oi@-Y;5U`Jd8|?2D%{q z@(}A5C@hd`lR;RNjAW6Kxn6QnjzJS64;vTI`z(ytDw$ZASQ`JbH2z}v!BEDKB6uWc ziO`$noidA<^ceQ6_Say)B4?_;`qjUy%qMHleqdl=oO9k#wt#_wg_*(Kz?DhCYO>Se zGXX9hf`-4>WbM4a{mV47GwWC-F5Zze`*`S>_#7rhhE@5?ekNVW6+QB__T_i(Zl+iGj1w~#oa)&7rT@T|u!J**6L{qdzhwSDesRrW zh`kDn88{diSQ$bJWnRJTH(ks6KRx#K|MTAd@@j5*mrfqpqoKjhV*XOe=1|a{S!aRv zx8!=uKen`e#drF~tbZ$~Wj44+eqr~z@YenNt+nAR=e$GC0?g?Q1}RJiEI%`Y(|yi+ z7O8J9kJ=jbQs?4kr|Ig&s~Y?*YUUjYn-zU?#c8?yC%G1=PdmpRwqQP!fzM3-&1Ek= vtj={w$yZj+*mu+7#kOO+f@>9YU%4G9yRb@(UFO0YK{lJL12Z+m)x>lGju|;x literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/9b46403b73e9c9e7.der b/tests/fixtures/diff_corpus/9b46403b73e9c9e7.der new file mode 100644 index 0000000000000000000000000000000000000000..b10e54aa25f93d28b5c0b659b17d876cbd15ec2e GIT binary patch literal 1277 zcmXqLV)<#%#QbmpGZP~dlMw3>lbKBC7+tRh;S%QcNi9(?R&dQrE~+djG2}7e0*NyVGY1(O8_0?C z8krfG7?>Cun^+hbMuEBJK(jzxgD3+jLkR;hhHpf{SH3~{GhpbX+hWe887}?{NVk*L(uB!i)JpTu(Fz%(~~!6eLq^Y z%{o1KhMK~r#UFN+>y<~oS@vg(s6*Xn@1y0_5xJtQtN(^GU3PdN(B5qFZtAMbe-^Q9 zUblD0t$F9Cnj3FEb^C?D1&^59(KTiVnHQCsPw|O8_-XN<{o$+LC-(gmi#l+#{@0g- z`daCnITK&|nD)Bw-0Hh=_N%#4w=({HmATEt%*epFxQR*5povM!fDagoviyvU|5;d= znOGMX2!r^lETHJ%&}L&~Wo2h(G~fXV3WF3e888^gf_Qu^Vk{zMvnm~}Ij?+qKTWgW zsBHV6_0g~H8pwmBm05rh)FASF^?ZIE;|0N&C@oNElS48(8OdlvBNM&kq8woQ;9=tedYOgsS|t+;6HDV? zmd0NUKN!jw)~9z^hi~w^+rR8)-8a1zuN%3J7bbjAi3`2PqyN0M-HL&Mac;yn)*J=~ z7G?%_16L-6v?t5HyH-73VR%D*_m6hTg|i(sHCuQ)^#RDhR=dKzDCc} z*#GgSK*G1!XKNF_{};cLpfEFha@{f3eZR{Y>KIn2oqo)9%v8uN*#1H4M}Ntid6FE$ zM`Um2Rx)fBvI$j(+MCJ1!N9=Ez}oN82DM+2VfxlJmP-$&Jn?v5yMMX)oR(Wo3&Yo6 znbSLY`kyZ|Upt8~DM(gLi@n1ov}{(_kwbCw6#Y)}?br2~^vQRQPCa;%pSieoyZY<1-4PmV z)YaU+zSwi%Sb5tsR~NOzZ$7uXH3`K`JyH0UA>>rbVXfW4%=4xD-5%WYo0}%Kz)Z%p zU2Vl;qmDCnlUEj8P$|g!`drWYhHG+ee^i^1!pCR7$}hcPXZs&*;W6h^;_n52*X#_I z(l*I>B+UC;Q`Do*yy*a}8snn? literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/9d1bc5d2dd75bf8b.der b/tests/fixtures/diff_corpus/9d1bc5d2dd75bf8b.der new file mode 100644 index 0000000000000000000000000000000000000000..6603ff83ab38675521f65400b8a9ae4a18aa8e4c GIT binary patch literal 1456 zcmXqLVqIg<#4=?8GZP~dlK`t^s+!BJI9n-;lK$B9ug@6pvT^7bTWt=I0se8R$SPR7J=;R+Sc|Dg*^P zDunn1D|i+cmuBXrD>yqU7#JHgF)1N?nvs=(xrvFN0VvMJ)WpQdFeOVnzv^dX*PgkB z7H5?1&R;)$(VN{Dj&HlG7$x1{R_YWr|Bm3Vs!MZcPFQ&IChx`41Np~vs`;h3biB50 zjpI?@c~rz{&a&15?^n&~LbbcXMPFa%`+nPJ?Y#1B3?7f_m;L>^=~jFB%`HdMyt%T) z)6Z0$=t&Srb?<55<5oG)c*N*Kw?|)2`rYVlBB72|+5Yzr7w_8tr}Mt$&b|wFr#wD* z{aG2{$Wm2hCljo&#vyH5blipbn=u}myRS%iA3B<_TI2frU$z=T3tq;&Y7?94sGTqH z`1Jk@@iSQ6Y(y6SSo?!jxzp`i=4YdX?)NM0MwKifS*E=# zyU%MKbj|4G{IsQc-Tl`Wrb@26->0v6{=Vn)+cvqayZd-nFTQ)4BkhNIiM-10)$IT8 zwOEzkE%#{CIk~MXIjAv2V&{vQk!M^@dAA&CIK8Q2eavDNqjz$g!S5^`` z!<2Edwf1C)*2T?dHy>WokXP@Kwr7Iwg+0@zw(4x3A-C)B$2@1tm>1sjcNPBrv(Md! z$G_%mYK!5tLx*z|;|zp=X-ig^k?}tZhXEUq0wxdxS&$eXix`W@Kh8pj zP*wq#8%r8Xcg&M|>BZJ@!ayD*t;`}}Al4w_rxc7$X}||k zzz;H$1(+$@Am*vEh#82macHwKva+%>Gs0O+24yfdBO^w7nK+|ft)GNVr^h)V7|a~ zfpME5%$TAaW1`H+fp|>;<|am9&~qB@UJ_^*@cq;_ z#bf-*N}-Wj2I|}w|M9y_THWzg`M9rAT7{GOMj!8gxAU01{_HsWtF|e9VvWA`yj?#; z?um%aicsR3_@M5wqN|VeMW(H58m=V4V^1h-+Ttv~$g3 z_txiE+ZFb49@3ohM$`7wyDPk2Uw>UaU~@p)zbgCCmml2$Tsgn}Oxz#M4u3kMb~ykW C?IxQ5 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/9f17b548f7d24f30.der b/tests/fixtures/diff_corpus/9f17b548f7d24f30.der new file mode 100644 index 0000000000000000000000000000000000000000..a6387ebc2e31a72639c3f403f0e1f5b66ee95dc8 GIT binary patch literal 2478 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 G_4yxVrgUcj literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/a75dcf6b0c306aa0.der b/tests/fixtures/diff_corpus/a75dcf6b0c306aa0.der new file mode 100644 index 0000000000000000000000000000000000000000..45f6a3c82baa07390ddaea678be0505a051c7a85 GIT binary patch literal 1670 zcmZuxdpJ~S7(eICnPFUJXk0^P)GUSZjf@FvT_#N-jpXv!idn`j3^U`lTP;JB=tkIw zE;2mJN~N}JtB_EXUDwq`8*S3AlvIe#o*^}V?0KH^obP?#-~0Z)?|i@K2c!fkkm93G z;}94@G@*g?Jw5Kz1Qw~Zi!@7A9|r0K;+o8jnN#X8tcr&~+F__mi4?|Cv1)=yfPF-X7#YU}tk6}r zW&s zS3#sO3^j+w7{>3pyqr>ozLpSTu z?y1M8Y+t4HaK!HK((7aTnnXYEf#6s9$M?mMg2v9X2L%pIf>T}}cvW!0+X<6_m-Qzn zFH4#q+xg+Qll*?-Vs|aR zfO#U}9)g1*xJ-)U0V&QC7-K)sXeEkWU#eDa&d!_L&SnoKJujXq#t+rOx%?quBnOaC z9aQV^cU<3wf8B2H=Mw3Az*s>G@B$FfW$J#?g5{BZ4kAlSXxG2ZZYFrfKq82$W z46O0r}Zoov!Jc7x1`++NuVL2!ZT_6i;Zp*V;O+z={fCN#})t|EQJ|MrQj7LyWE zP%PmWJ838F=^b3tgIY_31~mp-1=$>2L914MXrdFj|LW0BbG2Qy&BG3HT6aPKf) z{mkArvnt_-t2klun$7Uhs!mH0lU&oeBhJN=?%CG+k(d67mj+#GoVb0@*gim(axloj ztYjF^%ZM#zA5e!T+7b%KoXila(BfCJkM^8BaYt8h)saW9zMS9iJp0X0qu&({=-GO_ zyoK?S{PXK4Ybw85Yhdw%(#5GGe-__xjJKu_(kkiI{1z|TB5(OK1>Pxk;W}}ltFAQU zoZ+ZueHKx~wY{kCE`k45Idi&@(P(OX}=_HxUtTW01*=>0|xxIPcS0Q5jWp;XCICxGh^;M;v{w?Ka!c^Om^# zd)YC&$wzLt8kD_j=Co^^^%7iF+C13C$*vRI)%CNRybP6e-W+^%H9erZp)9XvwARgi ztCj8iK6Y{aec#4jVj_20xrzl!?G}?txd(WBB~Mc3rMz$JWV3zms|b>tB^3MS>HS~j Q`Q7mjJ0Dh>?>7kj2hs0l!T_8Usz5zJ zLGHqMkSZSFg%w3*#9FWwR6M|2TPji=FO;?$QJXlePu9%^CPO3~HiHP<7{Or}x|E2KTZF{OPI~3^%Ll%+#}=T?J{@clCHmJMO9?kU9q3KV&h=L?}vPtJv>^@ zEM=0at*uPCc0^{{5+2^?U70_;Fw469vnO!2;Yw*$9-gT2h=tD2= z2_FBlVj5Y~=v;ke-=o?9!}?%`-)8djOmddHIoi%>S>!orHtI`zSm?H$QBPIOYxQGT z^{H-5+tKM;ai*MI^rNpbcy3fOl?z3*QHJIgib(c%1J{~xTM-fr!Sy(L2jFNwa3Gp! z?~INI*6XYs=5OXL#ACB_mS45^Gj3!j=HD~~_F4iBwMMO&=>d~V^Zl(}(x*O;_hdzF z7pSe~|tczF(rRkVNk7PQQQkF!!zqn<(GBs7CAJ_Q*{l=~M zm`sYvcnZB33jDfUaE*N7NJ;149ZWEyol_U_jG?W-$COijoj=?HI{LOQIM~ zr@&;g0SN{^T9O0G0c-$jU2A9LG13(ZskfUOUnX*uvBiM%J`IITNvsmlIkz#CxChD) zfnXRFf;7PVU;{YM>3tlGFPDBk5b*d&M|5NrpJ#uN707_8HfOrFFa&6SIN20qq8=2! ztVU1-#bJy%8L$w_gd+Gi%I;U%YYJXWTJP7t_Xo|KdhV)a6(y(N)K$Jcbz%wx!4+3S zJidbPXD74^B7y2Lea8_M!R97Ff6mK|y6 zL7iWlKNVGRx(}*%T)MO_SK-}yD!~Qs*afK|yPEVFe~b(V3u9i{Ze5>$^E%u7YQ%$c z2kzqY!_Ratu6eK9{7yF%gb=#Jva&k-O_IXvqQ&9j5QtOJzAQ@ft7^E0T$&Xc^w_nt1NKbz0To>K@ZH52EB8&Dj&xwMQBepkXWviCKJ(RbbsprZE zP3+dZ>YQ&oGGe3h*|wo`OqjdsW5JHBUe}28?it($1NTkb%D|lkrxMKClFsiDH4bHm z?vCvwbgS%MJ1<;%Z}j1y_*pEiIb_z+ogJK*nv>;;*RgFjIcId=0pd*}-T@d|lwDs^ zv35daQqjg|n$Yn-7mFRj@Yd-~MsJ^#4+NIHIFd=9Q`<7?u}QvqIXKu*fb4B7>dCzy z#x+f&j-K2z$xYdC4hOm2#hq8Km@F}t?s}Qsk(?j;)5xK;&|`I9*$U$HZhuXOj!H>QG^Y+1>)S^w^Y~AT z)K$>*yD>YDH_LcS!!(Qk$zHO&=yup_-&@#>E=I#6kKykdQwMHbzFs{gtgUthC9`>8 z-{k}I9-pXQS;V;=9~6{x;g(6{h5o}6+9FrQXA8~UQre=ja;YT|Pfq*y_qw0GcIQ70 C@#$p% literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/ac8ea9f2874fd368.der b/tests/fixtures/diff_corpus/ac8ea9f2874fd368.der new file mode 100644 index 0000000000000000000000000000000000000000..a373c7cedba15e5c24dcdfb5bbf845537b5ebf7a GIT binary patch literal 1980 zcmbtUdo+}382`R+#$b#~%@7J>kS-hV3^OFQLb(ny?psSSGem}&5naSg?Ua%&CD&S8 ziimZoR8*2d+l{hKhp47flF*Ws)PA$2ZTFl#o!$N8y*$6?InVET-t+z*AXYvD#7c#Q z7z9QTq7?)|YQ!g;WcPgkRP>HoKpm$#PvR;WQ-@)K5(HLI)qpAvN*CH6tWHbG-cBqZGOz|>gz|ZNuo8q3 zBbpG3;}J0eLj`uGDj~X=uMYhZc3s{(k?PXrTrt0UomQN&_OiL9w_5J?HaS)m?)H7) zy>g*Hj=`L9DTre&|IYUZdC|M(p2o&Sl8Zl=Z=Z|nH*hlu6?N4Xoe0psGkyDH=n^-1 zAz|&dW-s1-(=&Nl!jRyU@;-Z_cG8k-FFFTzFx>a_JP%jie|hU#{feC4;fU0bxm@hC zfDz+k(&f8byOFYb+)#{5l1ZIyW!(fbzVbfha*FLoq;`d)g`#fkk+Zr#}SCx2ipp>bB_wOPW@E=5#i#$6`#&K5{4><0W zq^sW|Xq&Z3BKwm9N-b8Z)G)15yUFkcrJu9h- z@vz2`6>Dc>queS;Fsxe@%e|7f`KtS`&DV##MkNy@y54e!YzaT2+levQ5h#Wzgd=tX zHS9R{xcKO~Gq;{MMDC>96fPNkhoo^zz;F9q)&#m#Z^}<^$*S+qd)=JwoYtk&YdHBd z?X8GT2hS~&(;|Fpmg`wRS`_Gw*CkU2rITl?%wJ~*(1Ik11xlAbjnfKV7Ro#~qmX<*IAiOeiZJW!QL1N8ly7 zO5)pxD_IBzhTtP&M1=l`WT1imDg#XzevieXYLNgfoa%fWjD^KE+OPtFfPo@H1FM52 z`4Q@0=#C{1W8~W|SVuT>ZzdSA0a4*5V+jBcRB$-1h=5A&18AlIaVls!B_(AH4D>#w zCLvgYAs%8u3D6qI30exKE0PcuRj2?QdMJ2}6qpHC7wDvtdiF|OYk#B$Q-?i5m5<-s zv7xhMJrI>CB8;%2BS2ILL|2f23OQi3*k&f288Y zvU%ZxxW5r8pwGt?I)%|O1w)%%;~P&mEGrXgHZOLQzty~xFnQgl{eeeKO?ydh=(|;P z^%%A8w(`|sx@orZp<4Q_Q^s}#ji6lH%^k9CgxPU#^yQvMo4ET*PIS!lsgrv5-X1<> zIP^ZZ-o&c1wd9!jo%U}Ac%&W=WqpFrq{jUV{od6%T30FadJSYA#TDA^Fhf;M;^gi1 zRJvv5k?5@!ZeQ=;k-N+H_JPImIi{92;x{qESM^;uw9hDO18_$oLw>_J?AG)VK@<_-i;dAy8sqhieKbi_Ho`L&Z6Sq?YHrWzkkeo{Gcw`jMWg(D@8GBVqL zE6{E^>{xTLjUi?D77Mr=1+ji7>ig2}9_=?;cf#DKskOgjmfu`v-RYh(S%A(_-Pr1v;={nJHrpnPYZY2c{xy^N5PP*8S2mD(GXg!rDBtk#$3hsr3P=cCW9 z+UXo@ZyH@%-YfN&H4sAf^%7WqpqmL)0H`ztqGU(YAjgf=ZMM&zknL~5> UE2)Xrhb~`}wdOPGq-OHp0k-e`{r~^~ literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/accbf2b1744959f9.der b/tests/fixtures/diff_corpus/accbf2b1744959f9.der new file mode 100644 index 0000000000000000000000000000000000000000..338f8cd02996a3f1de51fd0fa8b65a0d49d8a01a GIT binary patch literal 1272 zcmXqLV))FR?K>|cBR4C9 zfw3XC0Vf-CC<~h~Q)sZEn1Kk0!zIk?lUkx)tl*lLTvS<5V#s5_1rlc#W)3nmHjoqN zH8KNgH830?Z`jX+mXi~=hUl!W*=UmMyGx<2Jm~ZjL+gvp3%chV;;@@0@;kFi z_LsJq)8EG8;9m1T!C#Us&NMIh>gXGF`@p0JKeNuP308ajg@>y#jO)_V-3CgMrzE%Z zmdx25a!38g^WP_$j=$6WyXb%Rcdmm>%!~|-i<_8a44RlE4ETUSD9g{t_@9M^nTd4) zI7nqxSwL~Yq0Pp~%F52nXutzfBn(o-WWZn`3*zyyh_Q%pZ!d_qzV!a{9rovk#mr0R z+C2;_Gmr;KE3-%#h&6~jUp=2+$9O^Tn%*e>1ZlG$Q4C5Ox zwTWkxloVL$>lYas>t*WYqym$MUVc%!zJWN%R6!O21HLBI=mw<$Sp|@T4LI1?(i?dg znHUZ9K!WlR(-$Z%kZ+SkGB+8?TtfrB)xyOzP>(;N1Tz>6K3lHVB^80TEy+7imZ!N9=EU~=g> z8w)dohk+ZDB13<}&;84{d_Hi?Pwe1R&hSEp=~g9gFBv-Rvc2_Gu36dusN_NNs+42b z*mZ^dlwE^m=J5tDGF`v+`V{RAg4WzOWq+tJlrxAm>#qK=Qk9?ogVj>k|0njJP`=Ra z{f$Kv7_IWg!e4(u9MEOMzyP=S^*@RK3JGy`KQ3R%dbW9w_N%&+Ju}_39lkl5YMf#E zZUwZs@n(0hpxlXwm!+~&)0w99++=ByUZ(%j&dFacQd_D|7dZz3voJ6RF)~cPdQ4=# zdjIKX_itxSbjV3NG5_tV+o6wXiT8HvG;!0$l-12YN`GbW?%3I!yqAB&+_%w*JI`LO zKM;1bO#i%X$4*Ic3zipRl54GFb6V9mf116(`P;fr|Fk^1qaJKGW)yc%|DUHg$>%G7 z**1>)`#HK&f0ScYRqXywJUC&yL1Km0?gvsIb^orac@#N+|HP{W`AZhO6 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/ade92438c82b13f3.der b/tests/fixtures/diff_corpus/ade92438c82b13f3.der new file mode 100644 index 0000000000000000000000000000000000000000..35308ca9a5bf767eba1656732ee263cda900f7f0 GIT binary patch literal 4991 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/aeb1fd7410e83bc9.der b/tests/fixtures/diff_corpus/aeb1fd7410e83bc9.der new file mode 100644 index 0000000000000000000000000000000000000000..3041ebe970cab33dd7ed96de30854b81a77228d4 GIT binary patch literal 1115 zcmXqLVhJ~BVzOVr%*4pVB*?I;^y4$_el8`>w9DQ5uk2cAz{|#|)#lOmotKf3o0Y-9 z-;mpYlZ`o)g-w_#G}utnKpn*45?1icD@iTNOD$0dPE9T?$}Fi=2udwZO)N^zP;f8G zFD)<>H4p|VWftb~3=VQv2+GedQHU@!kQ3)MGBGeVG&TT(C?MAy$Tc!DHnlXhj507n zwNT7J1Y#ktPil#Jv4U$}a#3YL2}}{QFq5mfL79O78#~a|=WLl6*;ut3Ss0X%y*gd!{7?U2&KE26S8G0mRb&dda7F&YSgq=iAsISkl<6cZx@ zL<=8_7>md{k9l|6WnC8J-&*uEp@gO4&>V&a19^}ZWflnou?CUKEs@Wvw>^ErSlqzT zaY8EM+soN`21YRR85vpB3{(u1V0;6nHnEJ7k^(Dz{R%_9Oud{`VEF0f7p3ciV@Fnj zg~x!)fP;-Ky^)8Ji4h#Tvhpkn266^63#1lEAnQ&>){UA>n45sU=$G47W9~h9&IYmC zLuYskt}XvGrIb0t{fpzQ@PgTLrI*}1zg-D#p2ZcGe|fs=dS*4&;fWn_?|0K`eYv_I_v2^wVU$@VfgE!pw_A&Tw zv*Sytv$jR|sxYI2pC&TB*0imvdb)1Kbtg`4!H)2A>+b3j#28{o@8x>-vkcFSv7M z*}nVB|6@1jtD2RW2Yzp0`Cm}({r9ebn_97FW_9eZ7yQ2@JXG#^XMg#q;k4;c+qsTy zZ$HerbnxcurVmYRx{R4pm;3(bKkQ&yd~(GtmcKg>M;yJFS;M+(d3E*7TWj4;eDq9| zoc#Xt^kvEIe?q;tv+Vm+VOqeslIz9!L)SY~t}{z}v)-zIQ|v#<@twR3lZ`o) zg-w{r$<KEgNx3>ZEBJ>g1PA*l zI6EpB85mj^$cghBnHrcG7#W%v85$Tx0l8)d<_0EUu3?lx4w8+b!G?kc{2*gFggN|D z%M~K?i?U%t>_|eY2Feh3$%H2s<)s!WI2Gj=>nXTo7ANPIr507{D0t>2>lq3d@Il2y%#7^JP7ExMA3Hfz_T@OQD%#ht-7oQLhiI79`BgVX zO75h1C*^FKS8tXl^?Tu*Ql$iie{w%O0{&$z@=!ET-nhKUg=xj8Rv5%*GGH)h>|^85W@BV!WoKk$X>2s8H*kRo7%;V2WR#Q? zSn2B*rzV#crRt@F1HBj+=pe`ICnqMS7M19y17(5UEH2JTP6P$9UUE^1fjQh1#x`xJ zDf!981?Xm=Yc>c2IZuJb-@w@BM&f%8OVan6=V@J5NU#ieiNsb9xNij(yAb7m@Uk}2!VtQ zGn0WlNVhVJgn?LtNJMpCsn);5hE)?B7}s5U!S0r4?*TEMk420{L_PQXF4m8GWmn9- z!W;6(!0PwJ!e)ae##%NmuxGASGO;kRG%@A^aVBFrLn*^)i|?YHH7yHW;-e4V{e3d; z)2zs|?p&Fyts72@92SXq&cMJp-H@gDE&~G#GlQFf3zLGE``wKn=KM7~s21^guYx7- zzT{~~*u$Re>=Bpp>Qx9;W>N@JO25{6%yv&TcSY*Ww<-_$SAO*B{o-ys$>m~->NfUO z3}p<54C}&OB^SHCtGBqfX@cYHkvk+@bZNS#L8@MtlGTdZQ zHtm0UPEee+bgBGrwR1n^_CH%RrPcHQp@VZDed^fGq#*jhEV;sBt#KOH^e>uUKCo6F zaBwegJFp;#qwj{AV)a^}y;C+G?AmZ4{7ChKH7tDcI(vE#mljo@4E)3ul&IEby&PyC z$h)QMk?cLIpc9rBb@Ra0Et)Rsp}B9*&%F66?X2)y=7^L(LKzXWnH0q4{LEdoYN@CD z`YB%-{d2dhs0rZ7ja7YPoVwC--ib(g~|8G(YXOaNp0Q#LLePC)iEY{0!8xzG#9;$vlQu4bc*X7q>Qj m?#(x5Nnuxzd@m~>pr_Qq5xPWa_lK&F=PKuP8r@>v>;wR($(H&6 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/b09f6b59d29b3ecf.der b/tests/fixtures/diff_corpus/b09f6b59d29b3ecf.der new file mode 100644 index 0000000000000000000000000000000000000000..e2d62bab4a9a975930e13183da999e3310096832 GIT binary patch literal 913 zcmXqLV(v9)VisD!%*4pVB*fPGTIqX{_NhyP^DQi0V}34_FyLb2&}#EIXUoFOY+z)_ zZNSOK9LmBb%oG}IC}to6;&2J``lObq7c02tB^Ol|l)w}*3p2Tz8_0?C8krfG7#bOx z8JZiIMS;2IhK69SX_SGCp_G9H$QWi}!Qzs{DMo zP=_!xvNJm|u>83C#p3?X0Ew=9&pv$odGn{c-`gz?f4nrdUDQ2$^{I%A>%xHz z>lZs1JX3qfd{I-#|EiTl*9z}WUABE&*cLZ2$rv;-Nf__}EtlnIWc<&pB(2OMVIbBZvd&}PopxE51^KrYJxwTKsW>!;p~1iiW-=oqi<*Ioff9^wz|Y8@Mtl=-2FL zH%@mISit>eIOn{Z{}0JH{-A literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/b0f330a31a0c5098.der b/tests/fixtures/diff_corpus/b0f330a31a0c5098.der new file mode 100644 index 0000000000000000000000000000000000000000..3b2debb7e51462c914bba879a82148964c081623 GIT binary patch literal 1122 zcmXqLVu>?oVs=}=%*4pVB+Oncm?<>akRK=ywhn8i<1gn1ux(0t!L-`6UX@jtYhb za^k#3Mg|r}#zsa)rp5+Q;=INNNL&LO0!GUiNI{Gih8Z2~s9+z|- z{i4=>=1BUqV3y{!$~D)P{Mw%$XyI3(nI(EpBVJwc|CgAzy>jtEy%A+X%E2l3BWE-z zZCCsF<#mOf_HB3OwKpp*xSGmS+n;v)w*KAS$6hrhvWM3-nN9L*^#8A(Oiz3M{8zPl z`a?Oxe?v&IMfEo$?kt@(Ut8WW)s+@AF*7nSE^cCs0)|MKfe`Pi-dt#gGh_SnO2iGdw8Zy(c_4mb9|G49~Xx~HOwqVMwWbo zTmyX=-+-}AIisYcz)D{~Ke@O-uLziek`oQ}62U2~q^PvGL@zl%*T4#{fvHUosv)_k z1ebb!m^sO*MF#dD*U7V37?>HDEHGMNfMh@pVFQYC41_@jD6sGta2as0v86ZiFfuVB zr$1m`0j57j23S1SH^%Q)#K7P@z)+%zSY6cPUGzH z@~e%Zbu5W{O*WeCx;*7p*hEj0*#$m!|8<({P2~ivcDQLw@XPx4M(2N-iq?`OVcW|a zmo8IVB$y^F-N63x&AX$)j2`)0IX2BwNLAWs)7rUQqJMG89oChzbE5++4+louWpT1Q zZwfLyxn|X++R1T^pG=gF$e((-VM?gN#++u4J8#5V*PJ~1cSrGZh-}vTFRGvj7+_2x4a zah}qvnl~ru&djzad8Ma{SXm2L+uxYf^9F2R8QFZ#mQ7%JcGj79>+H|)`F&LFaV@xX zDs}6&VCHkG@{PXryy+45vd@|OaXCw+3x3XSD*2RLvU8Dc?k~Za3Hi3X+l%ryNZzt= z_sp+fIp?+P8}aij)5_d}E&Oet>@iLcTjZePAtxKFx#L(tG-Jywy(Ja%&)zh?TovKR z^ho~_=iiq;s)row{2sNqmp6Rd=x4I&&e#9*-m!G6K1)5kJKn8%$@?1@8I~Glt#MhR zQUBPLsXsh)XI;rLb&(%J7iLQ2_L<&)f2>TMiJ6gsad8uqvq2M+y#XIE1ZDXd8UM4e zFf*|(Fc1duRarn$!J*B@$jZvj%xJ&^5)=k0VlrSbkOlGhSj1RFnm%y^oN^Jo#G$sF zS%vM%UcI_*Qv-RBv@(l?fmnmc@y4%z`#Nqi*!U(d3NTbza8CK3vOyfoWJX4oAcFt{ zbr|1(u}wOoq@=(~Uq4^3AUjhp9hgk?i}lNkjP=vP3=CA^3Ypp@p$an*3W3V>l8Z_V z#6S)ZWZ^g9Z9+*HV6Q2Fyk@|`#+Kg5!^p&FUXKf0}Fk_oGgHrGaDB$fLIu>RWh+Ku{3^RY5d6Wo}rB4w8eMP&YG5mF7eR^ z@BTiS_i0w-S$D2X*47QDMGlKZJZE5FoEvfDcqsz|3p0befh&`O&eU^9p2l4NbfG(j zwd`oZzc*|BmzI}KJp3kKRmJ?F%zq|Dh6~5vEZ%!=&zeU2?FNp$TMt*nS^qSi``~6# zl;ocz`OA6?r3^PXL#?OySN%UBCYe9QzjgJ7AAjQ7L)G|2FX|QTcJ&s4+S?4a*Ui9% zNg-fhnLd^Mw!%^>ytR}Af zdA#9@^ih@LQj3CfJsEFbnSIasaDcmve6W}#U;MuMgGmo7_pm%R*?M$F#A$)8MjXc# zOG6iYDmHh#^GRIx#ChGj{}=0j{~aPEQufZYMyMlyXHgZ4MyYh>ldrcWe`lUv{Bmus zseQb}rVb6uIh(f>3NtT|%lhd4YiB9j5^wRAgBRvs7umMu-A`Lyoc8<0KQ&g+A?o0zg#dd@+yDRo literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/b5fcf79e5bb2688f.der b/tests/fixtures/diff_corpus/b5fcf79e5bb2688f.der new file mode 100644 index 0000000000000000000000000000000000000000..f517663df402bcf47ce1d866cceb02ad922f6c50 GIT binary patch literal 1632 zcmXqLVv8|oVs%--%*4pVB*0Y0_ie&wk;M`oB_A&5J<^B}@{$@#ekkw8ze13i7tmWh##1I%D%WM_6_U^(RS?CZ8OGR@uvYzo$U zo2LcXrA;ZD@XXUSlAB|L^Sqs2wFYU4jO^}ed8`oo3GPE4GuqPOzQ9^r)W zQ;VCJZyPi*UpJ5kS}Uu}B4HrbAX3uWa3JmYee-+g_iCE2VwTmi*k%njfEvJNcwU&0;Vot;)0|tP|^aX zEL}rApgv5KOvp8s3*0ON{V}gb8S|t+;6H60gE)ZujrZbc=_$1$%@pO*t z>W*)6UAFhu^{;kWY+fBdd&=S83+MbfxKE9NfpLye-;6a33@pqH?gp+*3i)&9RqeAm zzV9jX@?WX@w8XyUlrAf{dqYrQYW2rFYe7dQMTSp-Ofl0q9Q1Elt2_JhE$=xd;H@4j zyxZ}$n!}R++#i1f?Uk9Iq4Vr61d^>?8K(WO+j8q&PUFN+Pe*5 zuS3gbKk>AB|H>M*H$4h7xAfeIzg#5Sy40^Hxa^=p>TICB@AvOF`FWo~`_}A({W~IE z|ISfQNLzC-Q|-gyfZjEQqGb%F3<*B7)>qY+$1`4G`!k96#LU;+GPOTW21)<=C0=el zz567@-kA+xd)*9Nm=stfXN8n=ER*(oZf0iRpEA2Q)7xrA?>eo{m%-1>#cZcBDR6lz zI~Tpa$#bmofymVxZo8eG*R+1;Hj2G)0rrEl&JPn`VfpT9f5TK?m!84`1hK1_HY znBKVTbcFE|>*ddFrb?ajHjT;Md7E#;Zkst%mWwT`72)muC(4|2$Yf=xjD7yu=UVbw zDXqWDt+r*o3!P){I8i13|Hn5AOlp2io%UQdK-KBMwP21-YzrJxe(mPp^tRhXF30bS z`NShH?}eG%fB)Czt&_Fet&FuPH=bD+zIuL&fwTJes{JO{KORt<_*ZF_`=U?I03I1R A00000 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/b63e5b2303d862a7.der b/tests/fixtures/diff_corpus/b63e5b2303d862a7.der new file mode 100644 index 0000000000000000000000000000000000000000..d319cd4030384071e42052c6102d196639c5d34f GIT binary patch literal 914 zcmXqLV(v3&VisP&%*4pVB*Z2Y%)+wC=ceg`Iesn~W;YH^HsE68&}#EIXUoFOY+z)_ zZNSOK9LmBb%oG}IC}to6;&2J``lObq7c02tB^Ol|l)w}*3p2Tz8_0?C8krdw8yOlJ z8=DvzM}fI!hL&KiQIvs#p`3vX$QWi}(emu)dkQg7MZn7^cW-l5t9O-&cB zyVj)^+va_X`&xK6=Rrg{qtw6Waqsz-S(i+_x44N((x8b+)PN7@09k%U#{VoV%uMVJ zKnsCN-(|w zQ=3>uNlAf~zJ98?UZ!47Dlk~}@{7{-4HQ6z3bIHVh&SN~E3m^AKn^$HU}H;f{P|#M1bcrSTKP2ZnNn z(-z-FJ8N1Ny2M8xy!-oP-ltiSXWh9nSz9-p7C9^u@tlEyajw7x({Bt6EX)iZ25wA> z4CnVWY<`#DcK+XmL(^+!9m&*LJXP$;b&jByV(0yC| z&wpy=F^I)xf?$i?3|yELST|i*cVmA?i%0HJj{KL7v# literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/b676ffa3179e8812.der b/tests/fixtures/diff_corpus/b676ffa3179e8812.der new file mode 100644 index 0000000000000000000000000000000000000000..04076dad5c846dc249839c13ab3a735ca929d55c GIT binary patch literal 1106 zcmXqLV(~L*Vm4dA%*4pV#LM_DUP<;(lJym1tJZJ>UN%mxHjlRNyo`+8tPBP|h6)C9 zY|No7+`{7SIr&M6Il-Cfc?v=K`6UX@jtaU8LB@u{27(|JT*6!k6)-_&WI+Qtab80U zLqj7&12BjZ=QTp+8Uz?}8*qZmViRU^ay67R5Qi8nfG}7guS~Z%(NN7m1ti5REQ=%+ z6zr(rAEpo->;rO}k%6IwK@+1AvX>ZH8JL?G`5A!XTue=jj10@8PG04aP-IZPI-URZ zy1OwM+1dR83Bpy4r>DQ`>q?yBtycMsWx3YxCAY$Mzgw{6Lv>eusJ#2+MBco@Kr2-h z4*i~o-#f2OI=_DNa(jcfTkk8b`}fsF{O^tAoBEdSY&nwEquc(bX#Yp%3AkB`OMF%3bq!UIjgR-w5RWogw3Rrdd2x{0%4OK zRxLgn^!w>i)@zv=F>E{E1$I8{;&b$!J!t}0=S#8d_wP&hRk`hW?A|3Sqqp3jiJ6gs zad8u)CNM-)4fudzD$CEv_@9LZm^RuBgg|^@5TC<<4M;IDG8o8$#Q0dmSVVrjum5zm zB7McZ{C<_*j*sokOq*XD$b+PnStJa^8bta(c-ODcb5&XA8}KW6R`TwtV^4o+{y!1Sh9#s+2} z3*=e!4Rj5(7ica}Z&QV8N-oO5rbV~NSPv*=;11HKz~W$FXJEs|0%S56m>3wrEN5aA zQv;b(479zxyd1-z)Pnrt%#!?~N`2(y1k5481Yem z2=4m5_Ws}EKC5(=+lPZi3Pd<-4nO#DU1o;Vq9b;|Zc7 zv-f4ou9r`*bXeT3u%2582W4Qj=FX!%jQ#rrg45SPt48$Pn`OC}8^@=m{3o_Fx^^)^* z4I&MM+1SC(U}A*2hnbO`*@=P0^WXE@n)p+*&+cbwJZEg*_@!j+!ZXn}%jcdx{aHCT zMk+<1q`v8x%cR4M@A-Z?RE7!MPHMWc{J?5iL*;JSsK>>No0!}UnwXpo_<&Z+@-s62 zX94<@y#Z(;kgv)D@&ku98zU<#J2RsJ4@giLq=?CY!9W(o<6{wH5t+)=tHf+pp1-kf z$G0PKx@&b4ItvWsLDI@BKwmY8OemjZnttT}iZd&pRo`r=tUfdOr-eZr%w$GJmLP)w z19ceRfU!+Fqokz3N?$);uOK^9FC7@Z`o;R?sfPN_@%aX-aD_~5l2C=22!%jpddWp4 z22vmg2(kzp2sUAaDA;=nAnzG)u(72#@-Q+n8km7J$g}7h=o)A*&|IM2rV2GY8O3mY z6L&{*%hG~O*Q9))ML7mdjLd9Yz(8VQyjID?!oYt0vDHmEkAVp zW&Cx9{Xd&d8b_r*ys(+ESp2|=qnYIl42*LFbOH?+7+9DY+zecp6x7Vrp8oRRG53Y^ zRr!@iF0864J8kd#zNz8S59==v_x@mHQn35EUB+eIe|vekf0d`sia0bpG*Ok}+Wg?! zzbX7e>N=kp${0>8K2m>t^V*%J*^KsCRYJLst^Ayq9&`ISeXV8bOxg3Zpw_y9t#vnW zWm2#`T{};Foy?&Vn@*~%pPF!UNf*ProzKcPO)&pjy6ke=L?%UsYfi!ur@zF#mXw_5 zEGw|hRN;`$w7a`q>;IZIzEHjz>5rUMm^~Okrus`tn8%gtUEocQIXWq+$M4(DccP8r yPT7@RAMVw^sLlhL`u@u~_n)QKt(v>VZ%T(fv*7y>@45HGrY_Mr%qGn`AFBaI8#Ar| literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/ba06d3d3e348fce7.der b/tests/fixtures/diff_corpus/ba06d3d3e348fce7.der new file mode 100644 index 0000000000000000000000000000000000000000..1c971bb3a0da54f22d3f6ba5a8cba0ade1a3e83e GIT binary patch literal 1090 zcmXqLVzD!5Vpdzg%*4pVBp_~kD&f`qJ^{5`f39l?Jn1vwW#iOp^Jx3d%gD&h%3x4! z$Zf#M#vIDRCd}mSWGHPQ3F2_@2)P%fCYGcYDflMlC1<1-1DS>b27Dk%b{_WN#GJJJ zq7*|Z0|}4_7muKGer|qBzJjx(f=^~{W=U#_p`w92NRF9D!qL%D!8x_4Br`2D8EB?L zaB5LmW^!t=ft)z6k*R@&fuR8yL;<-LhDOFfWNK+@8D-#ydPD>~FZ~~|4CY3a`%mbUJUW=XN zAjx-Zsc^xU_dlWPCjU!QaQ zVD9+7{O}}i$&kjyO^nkGniwa814LGt1?Z0kkp+TW%68K}vcGw>Uh{p^()tSqOSTxu z0(~LN$0Eie(%G-S_=WZYk&x=oVLQ~~(=E8?TmT2ZEI%XTe-;*C?6w&Qfs_k__#6go zKnfV+5F=Ds#0*5(IJDUqSy|bc8R0A@gT_f9Jqj$1T?UQq2JCFC>5V*$j0PNRY#;^` zg2e3eR6wYBFYL@R&z<`KwE%;XG)2U z-5FC|i4Q3k>4A%E*dJ)$ya`^Mbdufc0{Rd1%Y ZCGX4BK1oj|b%%}ScFzwui8 zXDDeP4t6JxfRgoDk zb8l_{*QC2^+q)9e@)tM0J9bh_QfXz6&iku5=M9^iMT-Pk(yyG7DO99d1>Yd+6XjGMtU z`M=|ymz+O%|GG*&wPCnn;dN<=Q-z1+8NU;kowT2N__ogdm^OLW*?U*kU)X4WL(J<^ z-KRq3|G{%-Y&F=p`>VV|q4v@}tL1gNn-)Dh60q#X(tfqNi<4EA>o0%PzrUl{hG~&_ z*=ikP0aHQnwV!9$OD5>R+&Y@K&(Neq_yEd+VT75_s;LtG+)ImyYKB;aRXV9 z0zMWo7Lg{wTnC#y+dbu^*o6NH$o0I*KO<(K4U!gQQ87?z!ib6{KD4N@11VNuF*h(Z zFlJ**Z{z_cTmwx5bv6!dHbz!fb|ywK>5P(+0xNy}^73-M6ky6s2Bu6>?O)??Zq{eoba~T;~stqa)RA77q#x@D4 zQ}dIH3s5XD2!yL*YIB8oC=nD?#V88(iS<(nIPb{{gMyaH0F+l6*tmdk!oqm1l8J?h zrHQcsh;taT7|Ix~=hg~bT>iEE(D9e?*BSQzY&vNimHP0)X2xRi11FAVmNPIg&YJxz zOo@Skg_*(Kz?DfMzxw>5>kpQ$z1PYYEc0-h$D#09;Rn}fPr6pbezmdATbfCcVTbR# zgW=B6C*7Y_g-rXOrxez9ujo`tVbtr~Nh0hl?2HWM42KNs!d)d7yS}TpxVLG7PDrr1tkah9d`MH>6s}mmIY7_TGNTGQ8GNeM!~4EBmt( zrYSBv&2M*g+vRluOahF_PC-R>84F^TxHbMyB9-?J9m zN%L;{k+1IL8qs^dNnoee{S{R{d9RK|cx_;dm}*_MJY|8(LOsWcF=2vIlTU{1$k229 zpAzI{Rl4ZQ%ZhKkvs~Y|WIT*^_^RUcv2;b!B9Hy~(~{yQa@2O6f2eh^=b5F}z3AQd zbYiO8cL(j7sl4bgd}BXdtNdTO)HYTb VflI8%FWr6ro9_tcw285i8v&x{fOr4^ literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/c68631bd6387e733.der b/tests/fixtures/diff_corpus/c68631bd6387e733.der new file mode 100644 index 0000000000000000000000000000000000000000..49f8271ed56311b8acf0147cf63e74c59b3076a7 GIT binary patch literal 1578 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/c93e5b7b18fdfa6d.der b/tests/fixtures/diff_corpus/c93e5b7b18fdfa6d.der new file mode 100644 index 0000000000000000000000000000000000000000..c4a9e15af4254e5ab47056c2e41aa9362204d5f2 GIT binary patch literal 3479 zcmbVPc~BH*7N4FOhr{7=I4c^*BQc_k4agNOb$}66?n4C??3w8qnwjZtx_cN<5oiyf zR#`S0kCrNYEV$YgB2^NXpD&`Dz2t9X0vYT?$jz*znNQ@#^o{@YgP_r)L~v2` zXxK+4DU`|-;==ss5Lm$xz6yCNX2eoZicv1Wb!UxsaeyHy3k|>Kw#8o%2u?&0m)&d`YC5M9eB3VS!m_ zqI>Sf1`GuwXlB*m)*NDIed(ooWIC_YfNs>ifyf@3;GAuho)UTcH}`(dSZIV(^q9h zw}-4ZVmk`wynppf=zX6J;W=!cvmhjaB>OdyW?Tik8-W{&j@0_sfByqBCptL!l_Bj|IgUnyU~yMr*Cca6N_)xEaGyJLa!L=&~3)YNx$) zR;p5^)3N-4U=;?0X3PkXcNLorGFTZag%SoMR*B(8l@4LU9PB8>N;4GbDumOgLab~E z&(K4QCUrF=-aSeJmL*p2mD7VyK@2yD7IK}OouQSfjeRg=5O&VrrLfXz==|% z9SGt9J%U>h%B0ep*pM7`QI0wzDpgZhke`yCq0UNGrD~k+=?UCvhI3mmz=Pl9{EXYbz*L6{DkYnh^NPWMV0sVA>_Oq~6u!!xV#?ep z(64SfS0SvQ(*l?^%9FHukXlc!4)@@enLJ2K73=2=BU6=G6 z&4XKsI#Z{2JDQv*sJD=2)apDMT$M6VP84%#z;GApDrGt8M9nT`7S}Z#l#kCS=x51rV>66ZUn*l zaU4DmU?}|ENNxlIIoiV$MHN9<-qi2Sf!qkgbGT2NQknqA!Lc%#v0e_gD|jkAMdm$C z28|l!C5A%ZDPtJ&!x=L;)q1c#>A?EL-JOw=lMnlI_X8m4KT@egJgUv{EqzZK3I_`s zB#`qYhU@>z$&Z)TZ-?u*!s%`E+fp|s^ZIp^Io8vFu$4KMva}eml7WrPv5;~57+A-m zqE+DD+nQ$nznbbB1a~1wx*D#ACkr$TZ4BpKC3H0DVF1o`N{KgWgn+;+MuFz>NZS7z z69db+R4KUBieYZItCjhJnnzP~C-AcTexhDO)5zW4}&pYNe zHk&ddwyOT9I07dp0g=x0ce{oX`i_LPF5P!@=H`aDUjmD!CeQO+8 z+9jHZJ(?%Ai8sG#QIGU?Pbi+)ReSm1SCyY!^Uv#d7^Z^lhSOk;SXq_wxM|=PvA#yU zzM!Cd=YU>W`@vwz>~j+vlwMnco5upX&0m!EZW=aMrj+>n(mLLM&u8$qIy-~SLz-#kBMrGD!V)|0^QtColRzZjRey5#YZLzG%P^^!F<=6qAn z02&tg+wi6*#qKLN_>BQpZb&q5sQlyo4J#fklia&q-TmNP_y?ukOV3Z0TjxFLNZgIg zk#||Q^jAzd)~b?6U5VV>+0}S+a{J!P!>1Ph+JAhOVPcQC zhaz{b>HKz9{^Ry$@BH15az<2JpuvjE2a3ERzl}c}#pKoO$;fJbI-Onj`%cTF%nR88 zdBjg(vgP2m+KCf^v&AW^z+=V4F~=UUE%^= literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/c9421441d1109cf5.der b/tests/fixtures/diff_corpus/c9421441d1109cf5.der new file mode 100644 index 0000000000000000000000000000000000000000..96aa16d21b37328eb78e71002c42b8b60c679ee5 GIT binary patch literal 1584 zcmXqLV$(5bVij4y%*4pVB*3>w-9o*gw(!!d1%s-RuW;HYK*B&uHaA`!Vw&2ueq!66*vEePb2s$0UpBdZE`wJ+i_s?UKyC1nq~Koj z{)Dt1!5!y2YD^uLo|j#``hhZMq=2}sPFVfZDrJG9v#ZS~II6F`wquXoWDR$n)zEmYT~ier6_SMh3>kP0RrXP0T(9^1$$uRc4Ve5Ni;T51N!1 z))Z3Tn07Fcocz+1w4(gH65ZmG#FETpJ$M{7Ngzok78D@KNFm8&=B1=oAj$Eg z3kaYaY#8(VrK4jH=(RgfY!4sA9@R#tXq zMgwb*B6$`Q10w^21$ql~+O#rCN(!v>^^=Qo^oopf4NUYB!HKq{sI<65FF8L~A0i5r zHzq+Rl}@JEi&K%xmg(GF(w00 z0GWaClnelXv?COqha$UCf*7dJ;S!`Y%KYPmI z-wWsbIk-=afq`-M@!6(K3=AyH3~mN4ObW5vPo~@2^C=i@l`$^K-Px4EP~Et*ia+Z6kpjKF>~UdfXJ`b>w{He5kc7U~AnCT$vOZW{cL?Ecm#=w^u=% zfAPzp13Zhu7_wws<)c0Xx-6B`aAQ(PVVbod?Ow+N(cRB1*X^Hq>u;^^p{Ur;4^4LX zoppHPzznoE!DrU`s`~PH#w%=pCh?w_`I=j%_Q%N}>0iIZ%dMw(pM=_b0&cH@S(;c# z>a3gm`)`*U9r3RGZCCc~fr9b&b$?qoAGCjvp9FMSua%nRp7a0ei>5c!uFtt6ZenIM zqwBnM$~>+_@*?d-$CESGGGl4SRPns|ZeUli@ktS}wBr z-}4d|_s<+|#_g}N*6WsI=5Nxd;1}*_Q`ysWa~Q z^P}e2&I$3BpNf}mz894`d&;h(ccy!;EJ(h}@WAB$>dF3hR)2c+HS5@%bjONp!(Dlo z7T>nEn3tL1x1;^deOBRl)n?0E?#<3HxNg|7Kk?|r5BAMRUuD>}H88tgc=V4i%T>$6 zXjb9z>dl9AzP(fVwQKhgL#esO$_mc54(#vit4tyuPl=kdRq@Z&VPs{Pom;myJ`a&7m8Ce;an;7{S44N3Zn3@aHzbQTHIERDpF(!1g)GQaJ{9Zplk- zye@a|@AU_#r!O{6V{hxNd-12ggQTEo);V)vP&-Tc!p2{-S^Rub$w8x&gcgr9D_VnJeCX{_;|J2P} zlz02&x_&wBDQX2hucLu6-|8C^Skma^IW)V_ne~}jhi)FnV1keK@Hao#ijU%z$=y=4>cw?qzQ zVDbWnG9yDmy2g}zK9A(rhIe13OI+=&wK>}DRsV9|ZEf-U8{&)0wTy%<8`GUyls~zq zo4)DT|D%6HB9Fdi`2qHg8zibR_3{p5LfoBYYm zyzW}IGxc}J?B04s;o;2qU^mg*8M8lpHn?hM_^7vK-s7%Yx2D;d3NMZS@UtMGz}-N8^%tYFG2u-sL@JUi+4LlJwK^__B94 Uy+>lhq8Hsv%iC;f{?uq20B`eLrT_o{ literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/cbb522d7b7f127ad.der b/tests/fixtures/diff_corpus/cbb522d7b7f127ad.der new file mode 100644 index 0000000000000000000000000000000000000000..232c4b6121f7236eb429572c591127e8aa60166f GIT binary patch literal 867 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/cfa6cb614dbd503d.der b/tests/fixtures/diff_corpus/cfa6cb614dbd503d.der new file mode 100644 index 0000000000000000000000000000000000000000..3e6174d29f680a71a70ca0e1d2d6b909c5699bcd GIT binary patch literal 1620 zcmXqLVhb>6VzpSn%*4pVB*0w0_4UN5sF5G!!=wh3Mx?EHBne1RJWCoS$pZ#HfVq z7DiSE<|amd2B0_>QxhX2!$YMynfz(T@-E36ucS4OL&rQxc|%A@9?!*1XP#;N&^!HTGGjSIT$;Tn@5yq;iIT!1?A-n52bzF8~F6Xr1nW?93s@MwwrIz?C^SY$<(+;C9|@LPn@Zf zsXcC&<^&D>Rr}0rcduEpY-R5CJq}wpdR=IIb3J9x!B`dZ*E1EopY5oa{P)qO&5hOy z*Q^dHo@(aN76@9L-+WO1m-ttQK537K{8?)?E_ldF*l{fJF!#MvJwtfIt7R{(HO-eC zxmkJ8Pxvy++M7!pce))~|FH4h`^|f&KNDnPW@KPo+{B!3(8Qc=AP)>gS!EUp1F;4X z`JhROVND_RjcGSzZMjw%#kAk$Fpvc);A0VE5ebw|d@<>IS@e<%u2XZkw*9^R_JxE& z<0_E6AWP#SgU0zyXmQab0gM~n(qi56)Z!9dBcxbr5-Kk**F%lDCUHZMh;B|&5mY5e z9vG!f!jR|%NhW7x<|Tp!#la?k;~iB3W@&07&{Ck0h@>G5a;pLhj{%nf2OC>@BM&1J zqX8dCke`w9KMM;p6YBy4up(8EA~p_fHbz!fc4kHcYmg#&783&_1A_&63v}AFGD=Dc ztn~Ghi*odejB*W3p!Sp$l@^zP+@}u_1@vr)SLQFD$iht|s4L| zyN$Q9ZzQ>|e;qBNVEZ6?rItbNYmdOE6M7H3i>iCfWm05V`dE79y?Yvg{HxzOhvrmV zT7LJ`w$Iu{H}7n}@#$6A{&ipn>+r2@lu0oPOc6n1$*ncCcBg} z3%0bhdzW5oTC7n=a>o#N-*{_D-b_l=(=Rn%5Uq%t1A9B;wI zXkV0G8)~j6c>`+iV#2eT8Gz zn1A0JFV8afzEsF|^?48XOWs?)Jn877Dbnkf`|V*5b&{GOl(vpFWqRlzuNfL*m3ywF z{aBiRH_ym#Q}{pYknrbR-*a(tOm9_SL~w)#Za=hxeX_OCd$v#h=9c!HF>RrQUJy61Zjs)iZd(7R{9YGYO= z!^y2Nfm?MLb3z~V{&&~e{h=}Kq>W1YEnd#jBc8v^mu{0+n>%;X!_?mMA0J%TGuV@{ zf6k-L58fQlm3G{$crokN`n?$st$0!;cJe!!v~5|XZYMdhv>_(V{mqPmSx3!oe<{yj GQVRf@x?d## literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/cfce1866e0240d03.der b/tests/fixtures/diff_corpus/cfce1866e0240d03.der new file mode 100644 index 0000000000000000000000000000000000000000..3d769644b896bf8d6cc8490635b999bd06fdfb98 GIT binary patch literal 2137 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 qtejgS$~P1ImVf5A9Xv3y{H{390t literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/d259c071b22ee845.der b/tests/fixtures/diff_corpus/d259c071b22ee845.der new file mode 100644 index 0000000000000000000000000000000000000000..0b38f30701ff8073b1a0673ca52a5fa98d578426 GIT binary patch literal 1731 zcmbVMdo+}J82)ZE7{g!?XH88>4Z?3+#-&t@8H$iWcBob|ZsW3MkUL`y?N)n9ZnbvW z$faB|vP6>mc37#SMQQg$my>L(RI6<5j7x{J|LocCobQkK{hr_RyubH-z88q_*MJCD zEhP6degf@P?JQgK-clf=Q4pPe4R43^m-? zbgW6bAzx}U!^3@MN$wgi!SF+RtZStwz>0Wc(zdyc~mR0Fzc6nSshSqp0 zU8-c$?Br`P=6668FLj+9nmtNoc1U~cgAeXZ{58S$=tM|nl(o*dA%24GLLOg_y(=mk zlU#z?C)+4v{8NX8!(ME!lxF|urb>B#&fXm>){8B`mFbqX-k_CHG3@{=fEHj`m z$oxT$T8Cv8fXqDLB4JCM#Ewg>L9u*`)wIhNUUaatxy-*Z`!2dyZeE#pP3WewbN^wGqfrBbFcfkIEQBi1Z>?{8YWSR_9ez-a5Y^AEZ&whyrFNZf`>DWM zMH+#uhq`!}v9=&}t>oFyYTjKy^){9rNNCBz0`J5U8ui~_4LRo=7&G9;` zJCD(|Wx#%Kkqk2RxWsN>xaUdYNUm;5a7wkeLU&caiPnd$qt;5$1LhPI2X)ZX_OW!& zvHnQ6xnKW{!&Gm_dhUj6>BE<+(l8qn4r~kP^i8+T(<&Ua&FFL;>%0`& Glm8FglWesB literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/d3b128216a843f8e.der b/tests/fixtures/diff_corpus/d3b128216a843f8e.der new file mode 100644 index 0000000000000000000000000000000000000000..6d4977b41acf80f199251fb42a598c135f8722c5 GIT binary patch literal 1289 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/da9fca34e821865e.der b/tests/fixtures/diff_corpus/da9fca34e821865e.der new file mode 100644 index 0000000000000000000000000000000000000000..1cdf44328bf170d65c1303505d91a9a59d9ba29e GIT binary patch literal 1020 zcmXqLV*X*!#5{iiGZP~dlR#we#JC2-6}R}_M87$xvSqsg7aNCGo5wj@7G@>`YeQ}W zPB!LH7B*p~&|pJ30~rv9OIXxBKR-PuRUxFPw75heIJKxOGdZ=`kO!z4D9dUQv4dT~#0Yf`Gb1~*69Y?)8jD`In#+>iDJFZp+>&RP+86K$`^fiuF*#xJg(`4^vP|ORqRJIX^EYv8WPVg9I)aNZ2WW!p?w$ zjV-;AhmncVzzpOXc@}*GT?6d}nhVt1RG}_UMsc}*n2C#lqi1$nj*%zO7dZw^j4W(i zz!+d*yjID?!olV`MVdfppY zslaoyrX**cq^cW}0#iKy`6NZV=$3bivSp3)x|k}Ow>>$a*xzr~WZIEYy%#wjGP^Sv zxG*UQclQX#d*67Se8iAd;6>4rUfw#MHje`t;$Ii}?JPfdmq{Vsv0VHa$LSdM^@Re; Y{l2mbj@cy{zQMitnERFW2TmC9vT-jdgAwvM(f`73^5BO5$$O{<_+&IewA_f-Nk&drwY%12pUQvRF?4~CksCf5Yc)7tNFALpw5#quM z+s(A@a~o|rWU+DLU7-!G+iDJMlucSV_imw(=rqQ;T%2Dw-raojfrXRFx6Eya*NYYZ zyW;%A`ANi2|IMG@T(g~GRdddIV`ywg#6YekuX zJV;uZMZ!R=LBvlfL_uQev&VVQ_<#M{DfE_2yx-D*52Sz}WFQML7q)@(l&mTXj{z4O zhc+7{D=RxQqd^&r&B(}-WsqT@0^=Jnwn=1^loVL$>*psI7ocP}0|&S&rZy9(s^mma zZY)Mos1M7Hkjx0mi|$5x$wegwP9Vq0vsfEg8kjFIU0~d12s5TA$CxNHav&a30C~uO zgN-e{kq21RAZIvW$->;k$jHFGXN$IZ$x-j+W=v+s);|eV9(0Zjt^Sypc)`aC*UFXR02u;C9|shGNIRR0dEf*c9=56NGG`EH7uDtw-3 zE6ljy(JvJa*Rb-7=_?JLE`(lVs==-%*4pVB*-xT^8fP{tsKDu+V%Gy&fB}mfR~L^tIebBJ1-+6H!FjI zwIR0wCmVAp3!5-gXt1H2feeVlB`oTmpP!zSst{6CT3n(KoLW?tnVec|$Ya0-l4llX z4i7RikQ3)MGBYqXGB&UPn0zkR3Dlj8KP&)ICXD_36RTHM6sWYEN9XTS%vS(cxX z@jnX-GZT9Q&_WGenUpZ*+3p7t;`}}Al4vqPg?G6rRWtqbCtu2yWf~t<~nUMFo=Me%*e>% zW8h_=2;&*wngWM_hdLchGo$UqgYgsDvusw5LdiC%J1iGd`@YC#qu z1A!*=palC%0pu?O4mP&*Mjl2cMgudDdU+Oo16>2{1)2-g+f<=uC!?6HpYP-r8JU<- zl^*5^v?s@)iIJ6!3m7^qjMpleSeRHEzp*raX86cZ&cGKwWzXkzEA5P>b!M_}{yyRU z>_fJNMyhueSE{Z_JnE;&z`!_HN??5_0|N^)gNK0|lOn@~q`v|}iu;%({NJVR4aKB`tlGJ0im3Pp#veQ|=?m zP{xoVcqC_u(3|C*GK-k>81}68*I>UQXR5yX)xWFECu`4sfLOfl6WC&R16QD_AD)-X zJQec$7W?gCQpLy1mKxKiSnS!qqd}{+`}gwfELSE4UZ3ja7qu?Ue);t!d(Q9bKAsMT zSLZL?**EFJ%XEhM4J~N-gt>{4k-@g~o=#(*yz$=^E9w<~e+ax$@1I(6ZMENjQMRhL z9cuzsDf_9PxZ*MG_{y0glP4RyzH|zCSZjO2TUS6l^qBiud)ErD)ep}+59yVOe;#VR zSoN3C!uiGf8pM^Z2At=YTr52CO}AfF0;8m!tij{PX-bU6n;yLJ2!G6>m1-2hy`x>9 z=koT4PRxG3Z@4!bMyizwB?ide*6H6jc}wrc;-sRi;s)!;CkL{m6y|8&5qTqZ+ghkZ zG%~hF&#rOb{0EjQ=kxZ7FFbfLL%jWB?WbN1^XTn+Cwb?t-`n{2;>{0pwl%+-c+j5Z joCxdJpYQk||1a=+=Mlc{hRd979Ief1ThAXhNwWd~U4n=q literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/e13650ac25e75323.der b/tests/fixtures/diff_corpus/e13650ac25e75323.der new file mode 100644 index 0000000000000000000000000000000000000000..cf7e7bcce91a46d77486a613f2826261cbdefcfd GIT binary patch literal 6404 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 P_LC(8r=1pKtIqxpFMO0f literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/e339a6843b0f4890.der b/tests/fixtures/diff_corpus/e339a6843b0f4890.der new file mode 100644 index 0000000000000000000000000000000000000000..63fee5a37a555fcd9c8c71f7de649865acd804e2 GIT binary patch literal 936 zcmXqLVqRj<#O%3%nTe5!Ngy}9X0lg;N{-8NE8X6fzfw^V|Muw(_hK6QQ24aRH20{?^+$ovG$@xVoddc~@29XBB zZ0uksFfl^i!pz9d?8Lz0#r(q4#B)m;+pDTe2BtDEPDo8-t!0_9b!Otpoj28PtV_CC z`*M;h`!wEJT@L%MpD#?ldh?Xyy@Z}EZvVOBj32o#Zej{DXkzj+-~(DK%g@O8p9Sbk z_6DGZK)xyq$o(AJY>cd|?97Y?JRm_~kRm1n1_N0TkB>!+MMU|(?XkFU^$$xvD0@o2 zdacyE!Pmw>9we>IB4HrbATptRl4<&p|0~X{d{%w4p|bkSaWIn^8Cil10u0n) zd;`Wd>5P(+0xNy}e7%C~Ouckq*yn6i z9U#a8jG`vw&}`z>(nAhhuum00J~iNAV@q%3VPs-7Fazn8XVEv%HPBw5xj?;56>3j1 ziaq+Feq{mXnV}vLz6L-$a}1gonc292;l;vut&)j_iKX!iOXEj|_Y7qWeBo2}d|tQG z&RAM!Ci~{^6YkGGWLs#YdRK9!>YBu(eu@kXjB|FThh#A@urM>Y8@Mtl=wCCR^Z&K} z*)#0b4sV@)U45&+;<}mBK3CnylN?i>_xms@GTgDBD7A0yzmF;!FEY*MdGM%pd}BLt z=g*bjlXzC;apwGBC}l_yJd(3S=*{v@nMF)`40~4lYp`FDGgV*x>fcr7leK3*K<#Y@ z+v{fF!lV$`+kQT-BGvd{QvFI5vp>%_B{gRL2 zq+9)#t2@-a{yn=Fv(mrYeZ5O(Ssjx?&N0lM3w+4edlwnj&P`h?P(8zdmyJ`a&7fHT%r)1T2z*qoLa2lZl`kzwWkIrXk`1uKH*pSt;!_nupL zcsOsy;j{hAO^!eJ+m%ffRy@jl)Z=HrG4fZ!BDu{c6@n$3PMHPgCiuQ& znE0oxH->|s>(HH##o0%N^SJ!04t-*h(c6;Te9Q2gZmGeh->VPh-ClWsZ|BeSD~DCX zOJ1{XdD*x1p2pcTRU%IcnNRyB>LrFBm0Za^qebdPrRL2|z54&dv(g@%TRnA!=CtGj zp>#Q`hqpH<7GAHJ_ji?e-aqZ%7UCio)0VHFwYA{PiR4DBo&#Dd%goO`W@2V!U|ihz z&!F+Q0Ut2jWce8x|Ff_FlR%q+EQqhlB4!}M#-Yu|$jZvj%m`;Of#XtE7^IxTfDK47 zF)~23@Ue)oh}@Hwds``b#m-#iu;T7FCYHHQn+y!(K_)7*NEnDUh&)-ZtyUh77YV60~HwGfT>L)qokz3N?$)yuOK^9FCCc5^otDjl8Z_V zv_Zzov#1yV6X6261u|_?P^HO8N+F7K41__76#fL1`Vww4 z&aE%nr=qq|j=^^?$J)t`E?m2hUuA6FKRNvI{Su*IK0(#Xv+vwHt)92}&`#qd%R>)L zA4V_d)RR0tQL8NGvL0GU-)pf-+Gyl*1;!# z{Gngi(X#(rs=_lrF)4AXIm+HDS@cx%^O0FkW3R2M`ulLZcACZS7n*6i-Y*c=|504` z;g*A-OzhqMWq9$GvBm{S zJW6|T>)@TWQL3xHxc>RbU9n{ShEK&y3|5#N46b*Y_SD1i^#nH_wmYXM9gF+y;^OS4 z>9%0mpPhY{>XsX$9&KFvNBDo=AGL>9{Ct a^z~Jg%vrS)f&WEjrgs*_%f49~br%5Y_Wl(B literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/e793c9b02fd8aa13.der b/tests/fixtures/diff_corpus/e793c9b02fd8aa13.der new file mode 100644 index 0000000000000000000000000000000000000000..2fa45b280e22a9fe641b25ffb3f7d866bebec657 GIT binary patch literal 1506 zcmXqLV!db3#C&W4GZP~dlK|u2T!TN0PX(@!-jl*-%B*X^%f_kI=F#?@mywZ&mBFB~ z!;ss6lZ`o)g-w_#G}utsKoG>?5a#krEm!bLEhlW?@yZTv2Ipi9%4Yqk?m4QAuW6 zW^!UlW`3T6V`)i7ei6_-137VCLjwaN10VqcQR2MD76yif7DmRVmZp|bgUP8)OiIWB z$;isU+{DDsV9>24f>r`tqA z?DrV|tIL`5Uu>?#-oQT7Wy{k}6s6x*Oxt*6b_Mf+0IL^gKHS+e&*$HI)@i2l;ZfWF zpZjUMU1!_3kh>?I3t9QD^Xb|iGi%Fs1=~~gN+#>-tTWOWU7j=gxdqQQPWV1$U$FUv zV&~6BhlAhWUiGiUFY|bIPr~GTM~?H{e`vJs%w+4pimB<{?`*^V@m$E{m*)Shs+Mh> z`#sP7SF!WcI=-c88#QZ;T9QLMkMJIx9AY3TyykMP`SRDXRyn3Iw;itw-CF6%eg8&7 z7w6=pzO8BJRXfhM?AU0(#FGCe)4bP0K2vKxXS1%16Kg)RBmL0};kjp`n=booejEM& z-qYeW%GSCJ`%ObjI6ZH8Mo869GJ6&{iSM&ot&Feuo|A8!)h_IJjCWY+;__wY^YHe0 z0rKG?EA0N3BV7E?r~I6|>Cbb>GTyIJ(92db)!TkcA+&naiHzi{X3 z8`~?tcH5t6y_R3UQuqE&rte>;&)RBw>u~ypcCVEh^OXDk$EasN+~EJ2{T+kemB{wf zL5@>jeR043F2ymlrZfNNdhjzY;(kN+JI(z$s<2>y5zjm6H7+upixM2hLgE>Bj zWv=~soVnHAtxTH9Z}wV-32o#c53jT?VXOxPVeQ|W8avxuYe g9fr>DI%KaO>$7prl3gxwuhv%VW75x46IT2M0J!OJW3T=5JW^WX&bAZBYRa^;1*AC4FMnUP)&9Bu?CGn5(zI88 zo%1_5v~Wt);iYlEkGr|WUH`;8IhU8?p3sW5TRD7Ww#hr_t$O`$k^V*f$BgciVok4W zvYk61TUNMiMFzw4!&#eMY5DZjOO;Rh ze8KjrKMdqS(#k9n24W2&3|lon>GtItzL@Q^w!-e$otHcpFBuraOlD+cQ8!REP=@gh znA*fMN=gc>^!1Ajjr210a#DdQLodH5UEd%ZWU3%bia}Bn4?MP$^K+Y!Jl(XsRKBpa2R40}eK}^hO>=CPo82kPGA?E?%Ix zK)y{D$*svqZnZSjOD@U*rZ;XjE?^L{FkY)D5T7iqpzm^|5 z{xbeL!~UO5Cyk?0A70qZSS)_v#L>)h1_s8tsgX-wF)*+&Gq@YLGAZ~jj?Z`Z`PqHF z#%=A*wx1q)xBOB})7IO*mr<%oThwICq{wiQ>D%nAS^NvuPgOm!caA{i;oo=SU8c7SM&Zh(-8B7Xk zOcw7sYSfA&ugoh+`~1+{ZHm#`%?o6<@a&#?@@4I=4&*EeEE0fOl99nH%(dv?tqbp$ zuq3{?AN!M=(>&>s$>y9WO+Vo&6IKMai99;F{MNgV->U34L{!||_-b!w+Q)UgcY+$X zq^wr$gXw|G}xHH$OdSak-wx+Uq&dt>eIktdKW`A(LMINi5;qySBW^ zORD^+bU#zm-<3!8p9NYkT6ipoA?n{$#-h`p);;6hlNjEe$!TwwvGZmMvk4O{Tynv|=$32ozFlqVFQ1u&E&sIWZ{E^_CzY-( F0swUS1bqMi literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/ea6b89ed6907a209.der b/tests/fixtures/diff_corpus/ea6b89ed6907a209.der new file mode 100644 index 0000000000000000000000000000000000000000..8a65485d0bb3c7544a22f948610e4a57460e9761 GIT binary patch literal 842 zcmXqLVsg$9Qx_@$PV=NDxg>KW*O%wFLN|SOjlNEwfi^@`qz*eT_mB3sOl%EfD zn2DJ|nSlTsJ2+&R7};308(A2Xn3EV-%6`pUnWymf=k@Z(?A9x8+s0eGSRJ}7%>SCl zt2gz!s}65ETKQ&XV!c4+ai*xBEaqQLOts6;)hqM|EoSI^b9MQm+~~*NlO``{_{uot z&W8+kxwG&7sRnITaF&t&_geCt^Ww!#j0y%#j4}rDKv&Buvj9ETAY%29v+c@x$wSl1 zN<7Q%JTN_$oIJ}w7NmfWMT|w{qS7s&$dBIfY8I>JUVHbN_wetPi3WTiX?{k=|12!P zm~S)S2l0hLd{zTyAO+E{$|7bU!p5P^#>mRb&ddmBF@a-VR)K}ffWv@|jU`l8fx#dE zWQ076r-8eH>jLKmj&1fCB_#z``ufR5IeMkVz?cL@t6p+`u0A}HfpLry#qb!`18Oob zg;~hR$f9YWZlDU|8!)y>LJiMPE-pYd9XZW1XEGS1F&Q%Sa4RHjzn>qUcI&mE3h(ODC22jC>h~i$GOJhzvZMjDZe+PoM8Pd8wOab6j@q8n<7xZj9rwBS+~?l+yw3$PofANYRjJS@ z7=p>tj;LVJRHjurV~Ir)!ony4#Mr?xC8 zW0W;U7oTsvlIXCug|G{oV0D`NpleH46JbZzdVXS`#CWcLM!R)5;RMM{(%h$(bi)^t z&%jSQNfklvPYqMcOS4D`?XX>opKP6H$MoTgE$l6K6`YYBC4=3+xT=HY>$(>WE#CH~48KH(lOV@nCU zveUQ8t**qsdri}F8&mQM=c9(=-l})cwfGsbNlc zb!ZY*eS5n^VXiD5_~7f>cP&#r_oUoY$?G(;&InT{_eTU}Eb7=?#GSF)V<{_@hN(Q< zK=Yl4Lce9LNK51LL$wh}tT20S-B|ycSx_8bbo6NyfJna_?F-eFgo+f-s%Z-t>g- z5($tV(MDKcq=i7L0#c<)TA-5V$$g?lqS!e!T6}!GE&sE8ZMgzIEp{zW5G$l{ zh{%Qk5al8}qFwEds24d{3|k~h?)xbfa%cR{TtEgBKEDMFV`MN2NJjzpGXzlbpl{Y4 z5hoIUyKiX`N7N2_>3p*SK=_z!LUM=I(UDp+p$iAoR?A>ayQ z|3;*Rz8+J^6h_7r3|W>}m*8L7cludgWxi_)Jv`AkGn^C>!qhXf`tIedE6xOs{KLCb zF@)za+vuwktMF+LnFjN%+ulF7e=o+y^L&1IEX&P(HW+l>$4P@7cv0QF@lNy1j`yhz zs_ZB-=cV4NcV+oc>-oK;{n}faiN*;T8JZQJvjRN1(`)6PHL>1FnI zVSvxEx=12ryqVPp@7WLB>u*a&{0E2+#p8qP;_NyzVE)5)V~spWyw4@HvVGI#8m}kO z1}j~mS~GeI6P;h=P-r<+#_d{udc@&0ojhfwM$#k$)#-0+cNUa+9C~rw^q_l}$E ORKqPxHHVj7Z~h$!Nbo}d literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/ec5c5d684fd5c4f5.der b/tests/fixtures/diff_corpus/ec5c5d684fd5c4f5.der new file mode 100644 index 0000000000000000000000000000000000000000..23aac6bf8d579a4095c61944b52376f35754309b GIT binary patch literal 1707 zcmXqLVq0#|#M-}rnTe5!Nq~9#3?D^*@wbd`&Tt&@opHs0myJ`a&7KTd~h=Lu6T}mYAHHl%Joimz`UBO}Yb%wabf}SmX?vSfmW(fi}x3vq%_-HHeh7HXKMhe&77w`MsLv ztC(f?y*(>#APZ8!$0Eievh1m;ug|G<9twM3wz-<|Xo$Q&mu=9v6(ldn(zw>3aa9wV z2T=HVsU=O^Ko2G*r{w9uImJm8Ag?wFXzAsmc)p1rD3V%SoSK)OS_D=CG`yk$Nk52R ziN>!&;`1PfWfL!oVFq>}XDhIn8<-jxv$3T&@&MDGfu?~v8;3R{EUqMSy-5vm^;9!P*#kOcgiec(qq`O z+FyhHikzwX>R11+GM}tH`+3>!pG`@IMY%2;c1(Jy+x zbL*XM-ZJH?{M5MB+nRO%HQWFyS$h0X#UifEHKBeT&UV6WQVVC@=H{+lvEtOf8T$+h zx}E|p7HigB{b8jlKmP}-rLO-^>_4G=q22o%i>CUI=kmtFUw=U@&PTF%_pQArl$1o} zlSC)2Dh^EXXD!<9yu`76{X&j)DwFGu-v(OzLEpH%EAC8R`Lm7OsTqvk60w_-= zyBWAJDX1=Rjms#u@Yb>S=C;`PBhg}kz3VH3U1vUTkYDKH{6v9C!T$HG|Nka6`t~ii z&RAspyNoGmL&v>Umn=6=dg1*x&@&sY++}WJWMts1DSBspiv2|~qp}Bw&T^Z?^2tli zX&Y}}_%$$Gpzc=q*14Sfmh9NBEVnhK)tm9+!bi=^xNGG#&%KLX7d3T?qmrNG*{c(^ zuf!C!{akgf@KROoA$wDBV^y^7C3b59m~F@9Mg=%{W-=dHyGp&KHd$X~CR)=^+&p`uN*3@35PT`TS~i z6!QJpx@__J#fSDbGhXsBm~wfO^pj}|Hweck>OTCCwWgIrFnja+pJz;2EsuOVwoEDJ s$-mZM59wuV52l>s7XCbW<%e5^!k3RnutyyE9D8T!7G0)5n~f<~0e|sQ1^@s6 literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/ed13bd4d797ad34f.der b/tests/fixtures/diff_corpus/ed13bd4d797ad34f.der new file mode 100644 index 0000000000000000000000000000000000000000..a1c313df221f56c8a2e8fc87e106417c232ad827 GIT binary patch literal 2620 zcmbW3e^66b7RTRx38CRPBq2e9jrybSwZ+9&L2y<&RaDsb0(8NoGdr7^{Bb|` z-0wN}ocr#%5cf)eIGxu-K>>;efVX}Z{n@6MiZY)M*L%~GMbMW<=T|PP{KXdlZw~~n z!@MD#M#-Vl7-&uw#)1rT;KlG+qAk?QG-fMrxj}`AAx2^fga0c|(;4%XI$oNHm%TEJ zmzkBy%UYHylEm{wPy~6hR1D5#`-jE;P+{&47WVk2e@z zGNn#iU^E-FN-PKlkkFGs&nht(Y@3xj4HgDNCu3x;)EbI4>g5KW%xE?l%}T4*Xuv`s zdotF5($Z3)&S|1G2vtTs#6kGX{R0n#f&+hq;9e*W0Mho{%3nEAu1;3{V_##^jpnhl znuXHLl)mII98o7bhAKyY`mt`+)`qHD_AYyLM(Xuf`PK0JrX$Z*W$!up>0`a`sZZ#O zZTn66x6kFJvuZ!qgzU`S&C@?#|1fmhMwI1;Kr0mIn_um*^!zlg3;yt86$+v&9rAe7%}ZEOw7Y?lI}p)_@ahgqw`{uPFuUwd%On#C1x8 z(4sD$O*u2qfIzQNT1w0s^2^2<7OT=)@|rHkq_pWZ2CM6aI7E^Kr7DZ-4AWAicclTO zyjDd-qf$Gs5}Y+j1Vk<|_gLf}i)Ucwln7b!|4NgE6S+H+OjUuKlR?r#vxy9jybY(W zx)k!$<58th0>w01q1}gQ1~3Lj)81G}10EipCSDB5r#!nX$5r zEa*qB_Xl8tN~NG4P0qFu7De@eUegK`0PUZUR&P@62cW$UE^Auaw50N#DOHtOC!Q@k z-8v>aN1BM}8${cf>>6a#AbN%X6B6rOmE!(iC41F$xByVMLK}>n9z#&cnF`gYvY2K$ zfUAGCLM!Fd8&Z-9vdl7!{a^QFg+64e7-T|GNOqhGnio+CD!6S!sVEf(M&e%#w8&

sAupTx;*U>LQPqH69-(+6+*mHEA8 z{C>E4$1#kHJnk4G{NB-*2V6Vyb7^Py!}snWXP)DK7w2kzxcUXKu+LuT(w0v8^?NkK z@6DyH<%c>0gjf6aNvc`BipanRa|G3ATh?)j zagL3hex&F+!mPR0imQHy0wWj9|FWfh2K?zzQ^N}_7qZU)1nvU z)KtCyjugaOwyo*^I>Xv8Y`NIj`{Ydhh3EI1uNvb2V!8cOMp=8qH`4YTWxp)h{`+tL zEo!%5;kp z4fPCkKvK-Ysz_3fB{_-3Ko=>vgeiph1cMxCWME{f5NKl1#HfVqHAYqj<|amd2B0_> zQxhX2!4=upPT+#^}{T${5vNV+m>I@+IFq(|2E%$ z?e#aax61ARqrKQdIN$aCtD-$i|CFEo^?v!!#>o?H|GrbNt9`U{C5L5FQ}hJM#}y0T z-YDLX?aG{7lTCHUq@c542{XVdTWmcQrQTyfBE&p*C?7Jhb#oP|3oKkT*HC+U9p z#d7av$z@X}^hNLbShdZViJ6gsad8u4JTOF}4fudzE6dNw_@9LZm`>UZWI=pY7BK@6 zHV$nzMpjmKW=1%R$v_AsEeul5VZa8Ym>3x#TKHJRSVT5B&oe0acJFV9oRF35gYSu_ ze=8g}kOyf|W|1%uYY^%G;9b8$&sAleZ@{nQS>5*@-Id;BPz^Jmk&z|eAlE<-#y4PW zQ_3hQDX`Ml&rdEcFw#p0C(B}BveZk?&($viCe0#a18cZ?rZ#=3`r_2&(xOy!4ateg zsYNAVeY!=)ddWp424*1F$+PGi=o)A*&|IM2rV2G7xhMx+JIFqWz8nKZkUj+#DFX=u z4mP&*Mjl2cMgtx;E?}6kFfK6SVq`{6guwg-OoWUKo6_gqTdidJVfy-A(XOu(yC;d? zjm|W6?Fc$0y~!p)P_bh}PQpIr9cD2nvmNdJn4T$fPfT(8|LdLf@+1xUhesdDTl!Vq zU3=HwUH3!ctO-fKoqwL1`ecP*#`Db0ezp&PQ*f{!_3N>w}k8euAl3lY|VM*rNy=IsYOH5&$y=#StjOx^5mSvbk?goY={5re>zta uYwl$9&ATOGFo&;!J$>6Ar8Ms-gS2HMa15#-_9Y literal 0 HcmV?d00001 diff --git a/tests/fixtures/diff_corpus/fe949f2d842955cc.der b/tests/fixtures/diff_corpus/fe949f2d842955cc.der new file mode 100644 index 0000000000000000000000000000000000000000..b470c2857627b4f990bafd4d26c5f8f5fdede9a1 GIT binary patch literal 1410 zcmXqLVy!c1VpU$i%*4pVB)}MXl12C5`GriYCbYg>zkZGZ7aNCGo5wj@7G@>`KSOQ< zPB!LH7B*p~&|pJh13?gnOPI^Cpdcqz!80#e&rr)i10={StOyYdC{4=AOjZa^Ehrv&oziN5N2Zs`-O=S>M3SMc4j9AmLvS) zQX;IXEA_XXpR+IP=(kP(t|?&9)@N#A%`TaRr1;*rK%>ZA6i^yk4G zlZ2z|m*joR%$w=HcySZ+V}mB<`vyEfugeNEGX7^_GGH)}2l13yBn-qFL~^D#xz0P2 zFXsMjhw$g(V5MJ`f@%hh>)1H7*%(<_*%=vG8W$Qg&NDECi5M`osb-Xv6jv6C^Df1hwcU7J+pc!*wyXsY7+;Cl?nW*#qJl z80#XeGY|#&NRWlkfTsyLWRpO`3M|pUAogTqOK;=>rX&M<16!CCOpIcNAS;T2o+&Rc zN3sIswam240`&!|ZOSnJ7v&(?g7AS}a#470@FNug8@GqC)6EmVJu9n3{P|#L~q0 z6o?-(-e)LdND(}evqb34@=lpWOnMA^R{LwPUy(CaU;XOeRpyhmXFo77FwRv9I;zaT zz{1SnZs5wKAa$^5XX4YKActl5#dr0rzP`_O=B7tJnLqX%G(B}(`}18UMTY4|CSSX8 zSmyD|O-&YWLzFHCKgn)nTJcx&&Yc&>k1%hsWGH9g3!k#*^SYIG#?m@7**AZmaDVn8 z+d?DNyNWAS*CZbGQ-sCyPnjllN!ySuMhjJKw2Ovl3J(n(~Y({YG)@ z_Q%(?{sEOdTfCuV(lv97Gcwm-CzK>QiZk`|7+>~SDl}p8lIcH8k1&)noVNHb+F8@G z&?P?l;N9OR^FGarJnPPt$=bT%w8&wRi04p?Ex;DL8MrVhRPPs4|Lz@Aa$;epeLd^6 z&ip3TdGh5!_dCz)&9F1ND9fafFkNoZJ+cL_t zF9NOgoHIM~o@~vVKW6vNwx=xH5xt(Py+QI~XwdJMuQl)fI1IIx4PmVUkL|MsOWGE% z2)wC&N4Du8@0SMSnXeOsnipG3H-AazV^VOM>b!CD_C*_cCF z*o2uvgAD}?_&^*EVRpx&%&PpnL_=W%L68W$FjsJLeo0AjN@7l`p{jv0NRUfdCb%TA zC@nKJCq*G7H8~?MKPNvuGqqSp!80#e&(O%g0HlmrSQDWvC_leM!8x_4Br`2DIk6;F z!LhU?BfluKq*6gw!QIF}PMp`s$iNJU!5~VU*BHb#G%_}|G_{N}a7A^nkbwZiC7d8L z6EpL2Dh-tk6d=}0!o`5@DM>BLO-;!JSs7$#(8Q>O92ShM49rc8{0u;GE~X|%MuusZ zJkmCjn6=f~vqO{=7S*>`-^7d4QbVxk9kYz`kO%2j zW|1%uYY?g7F_K??)1dxHZN}oH6${U;W{*}kh=ZBM$jB085Mba3;~OxwIc1cT6ju6|K!L4I*&Nq$kKesNk6Fdgcq8|ft%mFNNW8CZiHAkSiA z08Drb^cLu}X~7IF%E4k5vH>{;iXhDjEK&v%1{`c`>5V*$OpFFRY+S%_U}0=-lVoH> zPHe!82TW{?3>lj`>Zcw#&l<4(rH;oc%enI&ZIC-B6kxW0@%&8;bGPv>b5;oE^nP@@ z?W9x42flA1mb$VG`xN$M-JYkJywTRjGULLF5-*SH`!+|Fqja_V;|?%>FZ6u&xAEwO zMLd-rshj*6o(3@WueMKJkZH~=TDRcE?o(IyU-Z7o{cA&`<+Jx9ZRci*A6e)6*nMNw z@_n^+S1#PH4!*WZxbcO`NtJ|MjGC9S^DDFVymiu%V)v*n6^(eD#C)^7)pDb|;*-p# zr|brILw;8nw5o{hiDb6_Q@Nw=cv5c-^DbkF{d=!$`EpTfzWb71$qCw1^<}FVO-l3A WL$h;E|7r{3;kv&s_uH=kDJ}pvJf8{x literal 0 HcmV?d00001 diff --git a/tests/test_certinfo_corpus.py b/tests/test_certinfo_corpus.py new file mode 100644 index 0000000..73dcb0b --- /dev/null +++ b/tests/test_certinfo_corpus.py @@ -0,0 +1,241 @@ +# tests/test_certinfo_corpus.py +# +# Snapshot-style regression tests for the in-tree DER parser. Runs every +# captured cert in `tests/fixtures/diff_corpus/` (~130 unique certs from +# 101 production hosts) through every public `certinfo` entry point and +# asserts the output is well-formed. +# +# This is the safety net for the x509-parser → in-tree-parser rewrite. +# A new parser bug that would have broken real-world certs shows up here +# as a failing assertion, not as silent breakage in production. + +import re +from pathlib import Path + +import pytest + +from certmonitor import certinfo + +FIXTURES = Path(__file__).parent / "fixtures" / "diff_corpus" +HEX_RE = re.compile(r"^[0-9a-f]*$") +# Standard RSA modulus sizes seen in the wild +ACCEPTABLE_RSA_BITS = {1024, 2048, 3072, 4096, 8192} +# Curve OIDs we expect to encounter on the public web +P256_OID = "1.2.840.10045.3.1.7" +P384_OID = "1.3.132.0.34" +P521_OID = "1.3.132.0.35" +KNOWN_CURVE_OIDS = {P256_OID, P384_OID, P521_OID} +# Sanity floor for cert validity timestamps (anything before 1990 is junk). +EARLIEST_REASONABLE_NOT_BEFORE = 631_152_000 # 1990-01-01 + + +def _corpus(): + files = sorted(FIXTURES.glob("*.der")) + if not files: + pytest.skip("diff corpus not captured; run scripts to populate it") + return files + + +CORPUS = _corpus() + + +@pytest.fixture(scope="module") +def parsed(): + """All corpus certs parsed via every public entry point. + + Returns a list of dicts, one per cert, each containing the outputs + of `parse_public_key_info`, `extract_public_key_der`, `extract_public_key_pem`, + plus the source filename. Module-scoped so we only parse once. + """ + out = [] + for path in CORPUS: + der = path.read_bytes() + out.append( + { + "name": path.name, + "der": der, + "info": certinfo.parse_public_key_info(der), + "spki_der": certinfo.extract_public_key_der(der), + "spki_pem": certinfo.extract_public_key_pem(der), + } + ) + return out + + +class TestCorpusParses: + def test_all_certs_parse(self, parsed): + assert len(parsed) >= 100, ( + f"expected at least 100 corpus certs, got {len(parsed)}" + ) + + def test_all_have_recognized_algorithm(self, parsed): + unrecognized = [ + p["name"] + for p in parsed + if p["info"]["algorithm"] not in {"rsaEncryption", "ecPublicKey"} + ] + assert not unrecognized, ( + f"unexpected key algorithms in {len(unrecognized)} certs: {unrecognized[:5]}" + ) + + +class TestPublicKeyInfo: + def test_rsa_bit_lengths_are_canonical(self, parsed): + rsa = [p for p in parsed if p["info"]["algorithm"] == "rsaEncryption"] + assert rsa, "corpus has no RSA certs" + bad = [ + (p["name"], p["info"]["size"]) + for p in rsa + if p["info"]["size"] not in ACCEPTABLE_RSA_BITS + ] + assert not bad, ( + f"RSA bit lengths must be in {sorted(ACCEPTABLE_RSA_BITS)}; saw {bad[:5]}" + ) + + def test_rsa_curve_is_none(self, parsed): + for p in parsed: + if p["info"]["algorithm"] == "rsaEncryption": + assert p["info"]["curve"] is None, p["name"] + + def test_ec_curve_is_actual_curve_oid(self, parsed): + """Catches the original bug — `curve` must hold a curve OID, not + the algorithm OID `1.2.840.10045.2.1`. + """ + ec = [p for p in parsed if p["info"]["algorithm"] == "ecPublicKey"] + assert ec, "corpus has no EC certs" + for p in ec: + curve = p["info"]["curve"] + assert curve is not None, p["name"] + assert curve != "1.2.840.10045.2.1", ( + f"{p['name']}: `curve` field contains the EC algorithm OID — bug regression" + ) + assert curve in KNOWN_CURVE_OIDS, ( + f"{p['name']}: unexpected curve OID {curve!r}" + ) + + def test_ec_key_sizes(self, parsed): + for p in parsed: + if p["info"]["algorithm"] != "ecPublicKey": + continue + curve = p["info"]["curve"] + size = p["info"]["size"] + expected = {P256_OID: 256, P384_OID: 384, P521_OID: 521}[curve] + assert size == expected, ( + f"{p['name']}: curve {curve} should yield size {expected}, got {size}" + ) + + +class TestSpkiBytes: + def test_der_starts_with_sequence(self, parsed): + for p in parsed: + assert p["spki_der"][:1] == b"\x30", p["name"] + + def test_der_is_proper_subset_of_cert(self, parsed): + for p in parsed: + # SPKI bytes must appear inside the cert DER. + assert p["spki_der"] in p["der"], p["name"] + + def test_pem_round_trip(self, parsed): + import base64 + + for p in parsed: + pem = p["spki_pem"] + assert pem.startswith("-----BEGIN PUBLIC KEY-----"), p["name"] + assert pem.rstrip().endswith("-----END PUBLIC KEY-----"), p["name"] + body = "".join(pem.splitlines()[1:-1]) + decoded = base64.b64decode(body) + assert decoded == p["spki_der"], p["name"] + + +class TestAnalyzeChain: + @pytest.fixture(scope="class") + def analysis(self): + chain = [ + (Path("tests/fixtures") / f"chain_{i}.der").read_bytes() for i in range(3) + ] + return certinfo.analyze_chain(chain) + + def test_chain_length(self, analysis): + assert analysis["chain_length"] == 3 + + def test_certs_per_position(self, analysis): + assert len(analysis["certs"]) == 3 + for i, cert in enumerate(analysis["certs"]): + assert cert["position"] == i + + def test_per_cert_field_shape(self, analysis): + required = { + "position", + "subject", + "issuer", + "not_before_unix", + "not_after_unix", + "serial_number", + "signature_algorithm_oid", + "signature_algorithm_weak", + "is_ca", + "subject_key_identifier", + "authority_key_identifier", + "is_self_signed", + "public_key_info", + } + for cert in analysis["certs"]: + missing = required - set(cert.keys()) + assert not missing, missing + + def test_serial_is_lowercase_hex(self, analysis): + for cert in analysis["certs"]: + serial = cert["serial_number"] + assert HEX_RE.match(serial), (cert["position"], serial) + + def test_validity_timestamps_sane(self, analysis): + for cert in analysis["certs"]: + assert cert["not_before_unix"] >= EARLIEST_REASONABLE_NOT_BEFORE + assert cert["not_after_unix"] > cert["not_before_unix"] + + def test_ski_and_aki_are_hex_or_none(self, analysis): + for cert in analysis["certs"]: + for k in ("subject_key_identifier", "authority_key_identifier"): + v = cert[k] + if v is not None: + assert HEX_RE.match(v), (cert["position"], k, v) + + def test_ordering_and_links(self, analysis): + # The captured chain is a real, well-ordered Google chain. + assert analysis["ordered"] is True + assert len(analysis["links"]) == 2 + for link in analysis["links"]: + assert link["subject_matches_issuer"] is True + + +class TestCorpusValidityTimestamps: + """Sanity-check timestamps across the entire corpus, not just the + captured chain. Catches off-by-month/year bugs in time decoding.""" + + def test_all_validity_periods_make_sense(self, parsed): + # We don't have validity in the SPKI-only entry points, so re-run + # analyze_chain on each cert as a single-element chain to access + # the timestamps via the chain analyzer. + for p in parsed: + result = certinfo.analyze_chain([p["der"]]) + cert = result["certs"][0] + nb = cert["not_before_unix"] + na = cert["not_after_unix"] + assert nb >= EARLIEST_REASONABLE_NOT_BEFORE, (p["name"], nb) + assert na > nb, (p["name"], nb, na) + # Validity span no more than 100 years (sanity floor for roots) + assert na - nb < 100 * 365 * 24 * 3600, (p["name"], na - nb) + + def test_all_dn_fields_decoded(self, parsed): + for p in parsed: + result = certinfo.analyze_chain([p["der"]]) + cert = result["certs"][0] + assert isinstance(cert["subject"], dict) + assert isinstance(cert["issuer"], dict) + # Every cert in the corpus should have at least a CN or O + # in the subject — anything else would be very unusual. + subj = cert["subject"] + assert subj.get("commonName") or subj.get("organizationName"), ( + p["name"], + subj, + ) From fec339c286deb11d5ff1daef45f6593b238af0a9 Mon Sep 17 00:00:00 2001 From: Brad Haas Date: Tue, 14 Apr 2026 21:05:27 -0400 Subject: [PATCH 2/3] chore: drop fuzz harness from rewrite PR; track in #25 Removes rust_certinfo/fuzz/ from this PR. The fuzz crate was added in the original commit but introduced a build issue: linking the certinfo crate as an rlib (which the fuzz crate needed) made cargo clippy and cargo test on macOS and Windows try to fully resolve Python symbols at link time. PyO3's extension-module feature defers Python symbol resolution to runtime, which works for the cdylib wheel target but not for an rlib-linked test binary. Reverts to crate-type = ["cdylib"] only, drops the pub re-exports of Certificate and ParseError that were added solely for the fuzz crate's benefit, and removes the rust_certinfo/fuzz/ directory. The fuzz harness work is tracked in #25 with three implementation options for the next iteration. The corpus snapshot test in tests/test_certinfo_corpus.py continues to provide real-input regression coverage on every CI run; fuzzing is the deeper hardening gate that we'll add as a focused follow-up once we pick a build strategy. --- CHANGELOG.md | 3 -- Cargo.toml | 7 +-- rust_certinfo/fuzz/Cargo.toml | 29 ---------- rust_certinfo/fuzz/README.md | 54 ------------------- .../fuzz/fuzz_targets/parse_certificate.rs | 20 ------- rust_certinfo/src/lib.rs | 8 +-- 6 files changed, 3 insertions(+), 118 deletions(-) delete mode 100644 rust_certinfo/fuzz/Cargo.toml delete mode 100644 rust_certinfo/fuzz/README.md delete mode 100644 rust_certinfo/fuzz/fuzz_targets/parse_certificate.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ec76bb9..885c766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`core._fetch_raw_cert`** parses the chain once on fetch (via `analyze_chain`) and caches the result as `cert_data["chain_analysis"]`, so re-running validators has zero additional cost. - **In-tree DER / X.509 parser** ([#22](https://github.com/bradh11/certmonitor/issues/22)). `rust_certinfo/src/der/` and `rust_certinfo/src/x509/` are a strict-DER, no-`unsafe`, panic-free, zero-dep replacement for `x509-parser`. The crate is annotated `#![forbid(unsafe_code)]` at the root and every parser path returns `Result<_, ParseError>`. New module structure: `der/{reader,oid,time,string,tag}.rs` for ASN.1 primitives, `x509/{certificate,name,spki,algorithm,extensions}.rs` for the X.509 layer, `pem.rs` and `pyobj.rs` as thin glue, `lib.rs` as the PyO3 entry-point shim. - **56 in-module Rust unit tests** plus a new corpus snapshot test (`tests/test_certinfo_corpus.py`) that runs every public `certinfo` entry point against 130 unique real-world certs captured from the bench host list. Covers RSA/EC key types, SKI/AKI extraction, validity timestamps, and SPKI extraction for the full corpus. -- **Fuzz harness** at `rust_certinfo/fuzz/` for `Certificate::from_der`. Manual pre-merge gate (nightly + `cargo fuzz`); see `rust_certinfo/fuzz/README.md`. ### Changed - **Zero non-pyo3 Rust dependencies.** The `x509-parser` crate is gone (replaced by the in-tree parser above) and the `base64` crate is gone (replaced by an inlined RFC 4648 encoder). The Rust dep tree shrinks from **48 crates to 20** — every remaining crate is either `pyo3` itself or a pyo3 build-time helper. `cargo audit` surface drops accordingly. -- **`Cargo.toml`** crate-type now declares `["cdylib", "rlib"]`. The `cdylib` is the same Python wheel target maturin has always built; the additional `rlib` lets the in-repo fuzz crate link against the parser. No published-wheel surface change. -- **Rust public API surface (in-tree only):** `certinfo::Certificate::from_der` and `certinfo::ParseError` are now exposed for use by the fuzz crate and any future in-repo Rust consumer. The PyO3 boundary and Python-facing API are unchanged. ### Fixed - **EC `curve` field now correctly contains the curve OID.** `parse_public_key_info` and the per-cert dict in `analyze_chain` previously emitted the algorithm OID `1.2.840.10045.2.1` (id-ecPublicKey) in the field literally named `curve`. The new parser extracts the curve OID from `algorithm.parameters` and emits e.g. `1.2.840.10045.3.1.7` for P-256, `1.3.132.0.34` for P-384, `1.3.132.0.35` for P-521. Visible behavior change for any caller reading `public_key_info["curve"]`. diff --git a/Cargo.toml b/Cargo.toml index fe8ea19..8037e85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,8 @@ edition = "2021" [lib] name = "certinfo" -# `cdylib` is the Python extension wheel target. `rlib` lets the fuzz -# crate at `rust_certinfo/fuzz/` link the same code as a normal Rust -# library. Maturin still builds the wheel from the `cdylib` artifact; -# the `rlib` adds no published-wheel surface. -crate-type = ["cdylib", "rlib"] +# This is crucial for building a Python extension +crate-type = ["cdylib"] path = "rust_certinfo/src/lib.rs" [dependencies] diff --git a/rust_certinfo/fuzz/Cargo.toml b/rust_certinfo/fuzz/Cargo.toml deleted file mode 100644 index a41cdf1..0000000 --- a/rust_certinfo/fuzz/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -# Fuzzing crate for the in-tree certinfo parser. -# -# This is a SEPARATE crate (not part of the main workspace) because -# `cargo fuzz` requires nightly Rust and pulls in `libfuzzer-sys`, which -# we don't want in the published wheel build. -# -# Run: cd rust_certinfo && cargo +nightly fuzz run parse_certificate -# See README.md in this directory for the full pre-merge gate procedure. -[package] -name = "certinfo-fuzz" -version = "0.0.0" -edition = "2021" -publish = false - -[package.metadata] -cargo-fuzz = true - -[dependencies] -libfuzzer-sys = "0.4" - -[dependencies.certinfo] -path = "../.." - -[[bin]] -name = "parse_certificate" -path = "fuzz_targets/parse_certificate.rs" -test = false -doc = false -bench = false diff --git a/rust_certinfo/fuzz/README.md b/rust_certinfo/fuzz/README.md deleted file mode 100644 index 929546b..0000000 --- a/rust_certinfo/fuzz/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# certinfo fuzzing - -`cargo fuzz` target for the in-tree X.509 parser. Validates that -`Certificate::from_der` never panics on arbitrary input. This is a -**manual pre-merge gate**, not a CI check — `cargo fuzz` requires the -nightly Rust toolchain and runs for arbitrary durations. - -## When to run - -- Before merging any PR that touches `rust_certinfo/src/der/` or - `rust_certinfo/src/x509/`. -- Before any release tag. - -## Setup (one-time) - -```sh -rustup toolchain install nightly -cargo install cargo-fuzz -``` - -## Run - -```sh -cd rust_certinfo -cargo +nightly fuzz run parse_certificate -- -max_total_time=3600 -``` - -That's a 1-hour run. Use a longer `-max_total_time` (in seconds) for a -deeper soak. The seed corpus is auto-discovered from -`rust_certinfo/fuzz/corpus/parse_certificate/` if present; you can seed -it once with the captured chain corpus: - -```sh -mkdir -p rust_certinfo/fuzz/corpus/parse_certificate -cp tests/fixtures/diff_corpus/*.der rust_certinfo/fuzz/corpus/parse_certificate/ -``` - -## Acceptance gate - -- **Zero crashes** during the run. Any crash is a release blocker. -- The fuzzer's `coverage` and `cov` counters should reach a stable - plateau before the timeout — if they're still climbing rapidly when - the run ends, extend `-max_total_time`. -- Note the `#runs` and `cov` numbers in the PR description so future - reviewers can see the gate was honored. - -## Why this is not in CI - -`cargo fuzz` needs nightly Rust, takes orders of magnitude longer than a -unit test, and pulls in `libfuzzer-sys`. None of those belong in the -PR-time CI matrix. The corpus snapshot test in -`tests/test_certinfo_corpus.py` covers the day-to-day regression check -against real-world certs; this fuzz target is the deeper, slower -defense against malformed input we haven't seen yet. diff --git a/rust_certinfo/fuzz/fuzz_targets/parse_certificate.rs b/rust_certinfo/fuzz/fuzz_targets/parse_certificate.rs deleted file mode 100644 index 5f00753..0000000 --- a/rust_certinfo/fuzz/fuzz_targets/parse_certificate.rs +++ /dev/null @@ -1,20 +0,0 @@ -// rust_certinfo/fuzz/fuzz_targets/parse_certificate.rs -// -// libFuzzer target. Feeds arbitrary bytes to `Certificate::from_der` and -// asserts the parser never panics. Combined with `#![forbid(unsafe_code)]` -// at the certinfo crate root, this gives us a concrete pre-merge guarantee -// that malformed DER input cannot crash the parser. -// -// Run this target manually as a release-time gate; it is not part of CI. -// See ../README.md for the procedure. - -#![no_main] - -use libfuzzer_sys::fuzz_target; - -fuzz_target!(|data: &[u8]| { - // We only care that this returns instead of panicking. Any error - // result is fine — that's what `Certificate::from_der` is supposed - // to do on malformed input. - let _ = certinfo::Certificate::from_der(data); -}); diff --git a/rust_certinfo/src/lib.rs b/rust_certinfo/src/lib.rs index 756d32c..988111f 100644 --- a/rust_certinfo/src/lib.rs +++ b/rust_certinfo/src/lib.rs @@ -21,14 +21,8 @@ mod pem; mod pyobj; mod x509; -// Public Rust API. The Python wheel doesn't use these — the wheel calls -// the `#[pyfunction]` entry points further down — but the in-repo fuzz -// crate at `rust_certinfo/fuzz/` does, and any future in-tree Rust -// consumer (e.g. a CLI) can use the same surface. -pub use crate::error::ParseError; -pub use crate::x509::Certificate; - use crate::pyobj::to_py_err; +use crate::x509::Certificate; /// Parse an X.509 certificate (DER) and return public key info as a dict /// `{"algorithm": str, "size": int, "curve": str | None}`. From e7cfd6060d47f47e32de2533050bd688889ce9f1 Mon Sep 17 00:00:00 2001 From: Brad Haas Date: Tue, 14 Apr 2026 21:14:46 -0400 Subject: [PATCH 3/3] docs: state the zero-dep guarantee in Cargo.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the "No runtime dependencies; standard library only" comment in pyproject.toml so the same promise is visible from the Rust side. pyo3 is called out as the one required dep — it's the Python bridge, not a parser dependency — and the new in-tree parser is named so a reader knows where to look. --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 8037e85..9999edd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,8 @@ crate-type = ["cdylib"] path = "rust_certinfo/src/lib.rs" [dependencies] +# pyo3 is the Python extension bridge — required to expose the Rust +# parser to CertMonitor. Beyond pyo3 the crate has zero third-party +# dependencies; the X.509 / DER parser under rust_certinfo/src/{der,x509}/ +# is implemented entirely against the Rust standard library. pyo3 = { version = "0.24.1", features = ["extension-module", "abi3-py38"] } \ No newline at end of file