diff --git a/crates/socket-patch-cli/tests/in_process_redirect.rs b/crates/socket-patch-cli/tests/in_process_redirect.rs index d4f7dfe3c..076fa0485 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect.rs @@ -1613,6 +1613,87 @@ async fn scan_redirect_npm_bundled_copy_is_not_attested_in_run() { } } +/// REGRESSION (#1481): npm's half of #935. A workspace member depends on a +/// `file:` copy of the patched `name@version` (a packed tarball, or a +/// source directory npm links), beside the registry entry the run pins. +/// npm installs that copy from its spec, so it stays unpatched: the run +/// keeps pinning the registry copy (the warnings are pinned by the core +/// rewriter tests), and its in-run `--vex` must not attest the purl. +#[tokio::test] +#[serial] +async fn scan_redirect_npm_file_copy_is_not_attested_in_run() { + let registry = format!( + r#""node_modules/{NAME}": {{ + "version": "{VERSION}", + "resolved": "https://registry.npmjs.org/{NAME}/-/{NAME}-{VERSION}.tgz", + "integrity": "sha512-UPSTREAMupstream==" + }}"# + ); + // npm 10's lock for `"{NAME}": "file:../../vendor/{NAME}-{VERSION}.tgz"`. + let tarball = format!( + r#""packages/b": {{ "version": "1.0.0", "dependencies": {{ "{NAME}": "file:../../vendor/{NAME}-{VERSION}.tgz" }} }}, + "packages/b/node_modules/{NAME}": {{ + "version": "{VERSION}", + "resolved": "file:vendor/{NAME}-{VERSION}.tgz", + "integrity": "sha512-UPSTREAMupstream==" + }}"# + ); + // npm 10's lock for `"{NAME}": "file:../../lpdir"`: a link entry and + // the directory's own entry, which alone carries the version. + let directory = format!( + r#""lpdir": {{ "name": "{NAME}", "version": "{VERSION}" }}, + "packages/b": {{ "version": "1.0.0", "dependencies": {{ "{NAME}": "file:../../lpdir" }} }}, + "packages/b/node_modules/{NAME}": {{ "resolved": "lpdir", "link": true }}"# + ); + for (shape, copy) in [("tarball", tarball), ("directory", directory)] { + let lock = format!( + r#"{{ + "name": "consumer", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": {{ + "": {{ "name": "consumer", "version": "0.0.0", "workspaces": ["packages/*"], "dependencies": {{ "{NAME}": "{VERSION}" }} }}, + "node_modules/b": {{ "resolved": "packages/b", "link": true }}, + {registry}, + {copy} + }} +}} +"# + ); + let server = MockServer::start().await; + mock_discovery(&server).await; + mock_reference(&server).await; + mock_view(&server).await; + + let tmp = tempfile::tempdir().unwrap(); + write_project(tmp.path()); + std::fs::write(tmp.path().join("package-lock.json"), &lock).unwrap(); + + let out = tmp.path().join("out.vex.json"); + let mut args = redirect_args(tmp.path(), server.uri()); + args.vex.vex = Some(out.clone()); + args.vex.vex_product = Some("pkg:npm/consumer@0.0.0".into()); + let _ = run(args).await; + + let rewritten = std::fs::read_to_string(tmp.path().join("package-lock.json")).unwrap(); + assert!( + rewritten.contains(HOSTED_URL), + "{shape}: the registry entry is redirected:\n{rewritten}" + ); + // No document is written when no statement survives, so a missing + // file is the expected "attests nothing" outcome here. + let attested = std::fs::read_to_string(&out) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + .is_some_and(|doc| doc.to_string().contains(PURL)); + assert!( + !attested, + "{shape}: in-run VEX must not attest a purl whose file: copy stays unpatched" + ); + } +} + /// npm 7+ installs from the v2 `packages` map. An ignored legacy bundled /// flag must not block the hosted in-run attestation before that install. #[tokio::test] diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index 65c0d9067..b3341e0ac 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -1382,6 +1382,29 @@ impl From> for NpmLockTarget { } } +/// The `(name, version)` of the source directory an npm `packages` link +/// entry (at `pointer`) points at: the entry its `resolved` path names, +/// by that entry's `name` field, else its folder name, which keeps an +/// `@scope/` parent as npm's `nameFromFolder` does (npm omits `name` when +/// they agree). `None` when the lock records no version. +fn npm_link_source_identity(lock: &Value, pointer: &str) -> Option<(String, String)> { + let resolved = lock.pointer(pointer)?.get("resolved")?.as_str()?; + let source = lock.get("packages")?.get(resolved)?; + let version = source.get("version")?.as_str()?; + let name = match source.get("name").and_then(Value::as_str) { + Some(name) => name.to_string(), + None => { + let mut segments = resolved.rsplit('/'); + let base = segments.next().unwrap_or(resolved); + match segments.next() { + Some(scope) if scope.starts_with('@') => format!("{scope}/{base}"), + _ => base.to_string(), + } + } + }; + Some((name, version.to_string())) +} + /// Rewrite a single npm lockfile (`package-lock.json` or `npm-shrinkwrap.json`) /// in place. Factored out of `rewrite_npm_lock` so co-present locks each get /// the identical override rewrite (see the dual-lock note there). @@ -1409,7 +1432,7 @@ fn rewrite_one_npm_lock( // for every dep. Sound because a rewrite only ever touches an entry's // `resolved`/`integrity` (never a key, `name`, `version` or flag), so // addresses and identities hold. - let targets: Vec = npm_lock_entries(&lock) + let mut targets: Vec = npm_lock_entries(&lock) .into_iter() .filter(NpmLockEntry::is_dependency) .map(NpmLockTarget::from) @@ -1417,6 +1440,19 @@ fn rewrite_one_npm_lock( // Entries npm installs from a git / url / `file:` spec: see // `vendor::npm_origin` (#326). let non_registry = npm_non_registry_entries(&lock, manifest_overrides); + // A `file:` directory dependency is a versionless link entry whose + // source directory's own entry carries the package (#1481): give the + // link that identity, so a copy of a patched release is seen. A plain + // workspace-member link is first-party source, never a copy. + for target in targets + .iter_mut() + .filter(|t| t.link && t.version.is_none() && non_registry.contains_key(&t.key)) + { + if let Some((name, version)) = npm_link_source_identity(&lock, &target.pointer) { + target.name = name; + target.version = Some(version); + } + } // Entries npm 7–11 install from a dependency's own shrinkwrap (#753). let shrinkwrapped = npm_shrinkwrapped_entries(&lock); // The legacy mirror of an entry the `packages` scan skips (and warns @@ -1448,11 +1484,19 @@ fn rewrite_one_npm_lock( matched_any = true; let (kind, edit_key) = match target.section { NpmLockSection::Packages => { + // npm links a workspace member or `file:` directory + // from its source and never fetches `resolved`, so that + // copy keeps the unpatched source bytes. Recorded so the + // in-run `--vex` verifies instead of assuming (#1481). if target.link { + result.bundled_skipped_uuids.insert(dep.patch_uuid.clone()); result.warnings.push(RewriteWarning { code: "redirect_npm_link_entry_skipped".into(), detail: format!( - "lock entry `{key}` is a link (npm workspaces/file: dir); skipped" + "lock entry `{key}` is a link (npm workspaces/file: dir) \ + and CANNOT be redirected — npm links it from its source \ + directory, so that copy stays UNPATCHED; depend on the \ + registry release to patch it" ), }); continue; @@ -1500,8 +1544,10 @@ fn rewrite_one_npm_lock( // npm installs a git / url / `file:` dependency from the // dependent's spec and ignores `resolved`, so a rewrite // here would confirm (and VEX-attest) a patch that never - // installs. + // installs. Recorded so the in-run `--vex` verifies + // instead of assuming (#1481). if let Some(reason) = non_registry.get(key) { + result.bundled_skipped_uuids.insert(dep.patch_uuid.clone()); result.warnings.push(RewriteWarning { code: "redirect_npm_non_registry_entry_skipped".into(), detail: format!( @@ -19292,6 +19338,145 @@ mod tests { } } + /// #1481: beside the registry entry the rewrite pins, a workspace + /// member installs a `file:` copy of the same release, as a tarball or + /// as a linked source directory (whose version lives only on the link + /// target's entry). npm installs that copy unpatched, so the registry + /// entry is still pinned, the copy is warned about, and the uuid is + /// recorded so the in-run `--vex` verifies instead of assuming. + #[test] + fn issue_1481_npm_file_copy_beside_a_pin_is_warned_and_recorded() { + let registry = "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz"; + let tarball = json!({ + "packages/b": { "version": "1.0.0", "dependencies": { "left-pad": "file:../../vendor/left-pad-1.3.0.tgz" } }, + "packages/b/node_modules/left-pad": { + "version": "1.3.0", + "resolved": "file:vendor/left-pad-1.3.0.tgz", + "integrity": "sha512-UPSTREAM==" + } + }); + let directory = json!({ + "lpdir": { "name": "left-pad", "version": "1.3.0" }, + "packages/b": { "version": "1.0.0", "dependencies": { "left-pad": "file:../../lpdir" } }, + "packages/b/node_modules/left-pad": { "resolved": "lpdir", "link": true } + }); + for (shape, copy, code) in [ + ( + "tarball", + tarball, + "redirect_npm_non_registry_entry_skipped", + ), + ("directory", directory, "redirect_npm_link_entry_skipped"), + ] { + let mut packages = json!({ + "": { "name": "app", "version": "0.0.0", "workspaces": ["packages/*"] }, + "node_modules/b": { "resolved": "packages/b", "link": true }, + "packages/a": { "version": "1.0.0", "dependencies": { "left-pad": "1.3.0" } }, + "packages/a/node_modules/left-pad": { + "version": "1.3.0", + "resolved": registry, + "integrity": "sha512-UPSTREAM==" + } + }); + for (k, v) in copy.as_object().unwrap() { + packages[k] = v.clone(); + } + let lock = json!({ "name": "app", "lockfileVersion": 3, "packages": packages }); + let mut files = BTreeMap::new(); + files.insert( + "package-lock.json".to_string(), + serde_json::to_string_pretty(&lock).unwrap(), + ); + let overrides = vec![npm_override( + "left-pad", + "1.3.0", + "http://patch.test/lp.tgz", + "sha512-PATCHED==", + )]; + let r = rewrite_registry_redirect(&files, &overrides); + let rewritten: Value = serde_json::from_str(&r.files["package-lock.json"]) + .unwrap_or_else(|e| panic!("{shape}: registry entry pinned: {e}")); + assert_eq!( + rewritten["packages"]["packages/a/node_modules/left-pad"]["resolved"], + "http://patch.test/lp.tgz", + "{shape}" + ); + assert_eq!( + rewritten["packages"]["packages/b/node_modules/left-pad"], + lock["packages"]["packages/b/node_modules/left-pad"], + "{shape}: the file: copy is left alone" + ); + let warned = r + .warnings + .iter() + .find(|w| w.code == code) + .unwrap_or_else(|| panic!("{shape}: warns {code}: {:?}", r.warnings)); + assert!( + warned.detail.contains("UNPATCHED") + && warned.detail.contains("packages/b/node_modules/left-pad"), + "{shape}: {}", + warned.detail + ); + assert!( + r.bundled_skipped_uuids.contains(&overrides[0].patch_uuid), + "{shape}: the in-run VEX must not assume the patch applied" + ); + } + } + + /// #1481: a scoped `file:` directory whose source entry has no `name` + /// (npm omits it when the folder spells the name) is still matched, + /// while a workspace member that shares a patched package's identity + /// is first-party source, not a copy. + #[test] + fn issue_1481_npm_link_identity_scoped_folder_and_workspace_member() { + let ovr = npm_override( + "pkg", + "1.0.0", + "http://patch.test/pkg.tgz", + "sha512-PATCHED==", + ); + let mut ovr_scoped = ovr.clone(); + ovr_scoped.namespace = Some("@scope".into()); + let scoped = json!({ + "lockfileVersion": 3, + "packages": { + "": { "name": "app", "dependencies": { "@scope/pkg": "file:vendor/@scope/pkg" } }, + "vendor/@scope/pkg": { "version": "1.0.0" }, + "node_modules/@scope/pkg": { "resolved": "vendor/@scope/pkg", "link": true } + } + }); + let mut files = BTreeMap::new(); + files.insert("package-lock.json".to_string(), scoped.to_string()); + let r = rewrite_registry_redirect(&files, std::slice::from_ref(&ovr_scoped)); + assert!(r.edits.is_empty(), "{:?}", r.edits); + assert_eq!( + warning_codes(&r), + vec!["redirect_npm_link_entry_skipped"], + "{:?}", + r.warnings + ); + assert!(r.bundled_skipped_uuids.contains(&ovr_scoped.patch_uuid)); + + let workspace = json!({ + "lockfileVersion": 3, + "packages": { + "": { "name": "app", "workspaces": ["packages/*"] }, + "packages/pkg": { "version": "1.0.0" }, + "node_modules/pkg": { "resolved": "packages/pkg", "link": true } + } + }); + files.insert("package-lock.json".to_string(), workspace.to_string()); + let r = rewrite_registry_redirect(&files, std::slice::from_ref(&ovr)); + assert_eq!( + warning_codes(&r), + vec!["redirect_npm_entry_not_found"], + "{:?}", + r.warnings + ); + assert!(r.bundled_skipped_uuids.is_empty()); + } + /// #326, lockfileVersion 2: the legacy `dependencies` mirror of a /// non-registry `packages` entry is left alone too, even when it stores /// the plain version, while the registry copy's mirror is rewired. diff --git a/crates/socket-patch-core/tests/equivalence/npm_lock_rewrite.golden b/crates/socket-patch-core/tests/equivalence/npm_lock_rewrite.golden index c0da6d065..5ace6ba59 100644 --- a/crates/socket-patch-core/tests/equivalence/npm_lock_rewrite.golden +++ b/crates/socket-patch-core/tests/equivalence/npm_lock_rewrite.golden @@ -8,9 +8,9 @@ 20-23 b8ae78f255f6a932 36c2dd9b52c4f509 24-27 71eae0c3c0184ecd 58d66e565aa4fd51 28-31 01fe36b5faeb29ff 29fc8116aa94fb5c -32-35 cb046b19a5fdfdd6 c5fc2510910daa71 +32-35 cb046b19a5fdfdd6 e84c747f1b94fe84 36-39 efaf1af6b3d201e1 9ea5d8d9e0d0b7bc -40-43 0b146bc1275f5605 6dc1a2f316fea439 +40-43 0b146bc1275f5605 7972e2cbc7afe37f 44-47 6131178fcff5f168 fe7bcd7668916be4 48-51 7f23e01097535c51 7f6fc1a7d4b046f9 52-55 0d5e691dad879076 7604ade4a0a89219 @@ -21,24 +21,24 @@ 72-75 18775954a617d9ad eb8e0a1834f3c4c9 76-79 cd2aac77dd5fd50c 6ad26aa874db0b3f 80-83 6914eec135779154 103252c10e0e4796 -84-87 23d6dfca3dc60d0c 078eeb46fd92ff43 +84-87 23d6dfca3dc60d0c 4b5b319ea04c46e2 88-91 174afe518c036993 58d76ad81a0736f1 92-95 55c49569b1ff28ea 2c66448481270ec3 96-99 e9b8cddfc47c2410 e45224d4571298a6 100-103 21dbb73e40e5a210 af5e7560a9d0c0bd 104-107 0c170349f3052ef9 27e2b78479f64dee -108-111 b9c3e6c7cdfad95f f0148e213da61805 +108-111 b9c3e6c7cdfad95f 2f5f52b4659153cd 112-115 b7f86471de74f76c 392efc30f7f532cb 116-119 5b317616e7ec450d 892261da3954f566 -120-123 1af385991cee8693 985f1874988de52b +120-123 1af385991cee8693 6e0e39495e753304 124-127 45f394bfeb103203 1903864d3d585314 -128-131 32b0731afbcedaf7 80e4b2ec800f162a +128-131 32b0731afbcedaf7 966fb68dbaddde60 132-135 bf40da5a6099a8ff 244c2f2ce05cadd7 136-139 98773f82425a6fd9 9011cd7a515c0826 140-143 7635eee1f74b943c 9da248d486b3c382 144-147 43ccf3156a238f1d 55da03ffc362d938 148-151 bd11851bf97a2c30 c8f6b758d93c1d43 -152-155 219d9afb7f6a45b2 7f198d2d08e2d656 +152-155 219d9afb7f6a45b2 e45485c3ef6b9658 156-159 7c1e1e997c16b02e ca68697c227ebea9 160-163 143279d02db7a456 11e2adc2d8e75624 164-167 d4adfb48c56be5b6 a3fa28797bc91236 @@ -47,7 +47,7 @@ 176-179 2567d140ce3ceda0 4a9307775128fdc4 180-183 8c63f24d3ac472f6 cd0f2d2f7aa925a8 184-187 768a53cbd61c832c d64742504fdaaba0 -188-191 1b79c77ae63569a4 fc9fd305e1ddc085 +188-191 1b79c77ae63569a4 df5fb16dbeee42de 192-195 a652530b7d9610fc 7ef3c98c60d82ab5 196-199 e81b9c1564f40cb9 1834aaaa0f9cf4e5 200-203 ac3292d6054c9914 45c7b2ecb0f2264f @@ -69,22 +69,22 @@ 264-267 f98b36dd3f4431f7 66fda399b9e04c42 268-271 2242e200be734689 961c2cbf0c72226b 272-275 9fbf78e7d41a7e58 543f347b65a23c70 -276-279 ea0a7560bb20466c 0c715c2207044d29 +276-279 ea0a7560bb20466c 344819c0395d7b4e 280-283 3ab69851cae70bcd 8e480b61d81592af 284-287 1fdb596cc87f031b 64578f1e84310366 -288-291 ed84081f4124d522 68797a18e99c2dbd +288-291 ed84081f4124d522 f06108e2c24dc456 292-295 18bc713c335e243c 9c8a170ff899c58b -296-299 658ce14ea92fffd5 536e77ae46114631 +296-299 658ce14ea92fffd5 3e048476cb3be792 300-303 9a8fde3916e27342 bab49d863a06a18e 304-307 ac9ed568f15ba5d7 ee0cc2e069d5e0f9 -308-311 8ddc66826aca8737 28296ac8c39c5dcf +308-311 8ddc66826aca8737 d01a1614d0247062 312-315 b5092b2eb6d28e04 399dc984fbbaab55 316-319 27d1154fe85c9091 281b9314ec579043 320-323 8f7ded70633dd0a3 2ab2a92cb15c09cc 324-327 8c22483048c74dca 2e1a0cad7e7f49b8 328-331 3b6a2eae6af90be3 45de24588519ef65 332-335 291ee053b410dc25 4821eb617b8d86ec -336-339 c00ae81f0a2be968 9f069a599d03be8b +336-339 c00ae81f0a2be968 89f6b7eda51fe3ea 340-343 1b3d34745caf497c 562af394d0cb24db 344-347 8e350ee856023a1f 7f3b5b09c26ad562 348-351 7b0511648fa4447c e0f1d4a90bf10bbc @@ -94,17 +94,17 @@ 364-367 f8819f4cbe245541 5b51feb62d76b033 368-371 3d30266cadaa3712 44f173d7028a72e7 372-375 a65f748f7e3f6933 e612c7529ecd9fc1 -376-379 ee1ad63fa3935a76 2d0b8d59a4cc6d7c -380-383 0c76dfd9b908af15 c62f2970357958d4 +376-379 ee1ad63fa3935a76 b5959ec08901a56d +380-383 0c76dfd9b908af15 20cb1e0cbd34cdd7 384-387 7d113d1c4bcf7ede 694120e2b01d24db -388-391 392bfdf6873fb038 f681d116f11dc011 +388-391 392bfdf6873fb038 a8ae717944f9aa4f 392-395 8ab3f5ef6c2260dc fdc1bd49d88b0f00 396-399 e70ee73cf9f93cdd 1fe4da6c539ccf86 400-403 dda6b03292721d0b 9795245a300e80da 404-407 cd2441a35b1c5740 6fd4d2a386eed373 408-411 91ac4721e77b4500 0bc11d72a0297bb9 -412-415 28af4189511719f9 3d29075156f4c9fa -416-419 d9e2eae424bebb3e 97225086b3cde613 +412-415 28af4189511719f9 ddb2206e43e55a79 +416-419 d9e2eae424bebb3e 6be312fb7042abe2 420-423 0f86f1432c081b6c c8cd109152c9000d 424-427 6533c923cb60e539 88649c02c40ace42 428-431 80c11eb13664c5fb 3875f53d37e204d1 @@ -119,63 +119,63 @@ 464-467 1b0fa950663ac590 e1259bda18595a9a 468-471 9b8b2340d8531f3e 5cc104c917c7cfa8 472-475 736df961ac44dd1f 6e0b231581610bca -476-479 71839c074d059821 e32e1ee9c4690986 +476-479 71839c074d059821 91203a0525ff5d5a 480-483 2dfe4501253d999f 3f2903a8a1f3d955 484-487 6d4cfdfeda102637 f243f89ebb53ccb8 488-491 8e04bbe242721495 4827195052919674 -492-495 4b804da1bed766bc bdeb1a87ef9305c2 +492-495 4b804da1bed766bc 72ccae5686528cd4 496-499 e4358ad81f66c66c 1bd273b112273309 500-503 d497faab2e6374d1 25ff9866dabb19b5 -504-507 bd134b4a4fd9c1af ec0b3fba5c88c493 +504-507 bd134b4a4fd9c1af 1367ab6e829c8629 508-511 3a1c53db7ba9c199 fa3b06c10bf6d6b0 512-515 9ac76bdafc788702 1b12e2367ef70fd8 516-519 cc5cb99242e72eea 2412c7294ae0e7fb -520-523 df8749c4f1d4c88d 2151d47d975cb37f -524-527 df1f4eb0a17e6840 a8c5a985520801ad +520-523 df8749c4f1d4c88d 0d35d0cb8daa69c3 +524-527 df1f4eb0a17e6840 34b3e746a9db16b3 528-531 5bd935a1966450a1 5405033fc13d5f62 532-535 d3c766b67ad93619 0eb262d5010e1fca -536-539 0abea7838ee8c651 dafd54e8801c3793 +536-539 0abea7838ee8c651 f1cd1f6775fbd303 540-543 5a34e67ca32acd76 441ba16c8b63b7b3 544-547 8139c395ec116abf 1e3325d4109dd7b7 -548-551 7937635862cd0cc7 cdda5e17b4f48a72 +548-551 7937635862cd0cc7 7d895801e7f94082 552-555 f18bccaae83b8a26 d0df0c9009bc78a6 -556-559 0d7281f2dc6cab7c f88a0cefea8fd9d1 +556-559 0d7281f2dc6cab7c 75df07196c1d5198 560-563 67b3e9f59200ece4 adb2ddac66bceede 564-567 eb7c20329e96b501 a3ca918e8d58299e 568-571 d293cc2b575a278d b4e3b6ca4d70ca48 572-575 c17b85264698e947 edcae00b1fdd32ad -576-579 86db374e9417a100 b2589b8c5862526d +576-579 86db374e9417a100 b757fd3fd011934d 580-583 ce0fc0bd2408d817 03db723dd12019c4 584-587 436c95276aa8074e df1bbf8d7663a3c7 588-591 c09216f32360676b 66b0ec275885c59c 592-595 2c4e9ab4e7011bcd b690d76fc146a3cb -596-599 cc8ac6e16d3c70a2 a769d6b0522f7d3f -600-603 5e61ab66a7b47ef0 0a0039c9514fa44a +596-599 cc8ac6e16d3c70a2 99de81bd6529e4e2 +600-603 5e61ab66a7b47ef0 3309f43222c21702 604-607 890a631fda14796b bdab2de6ec82c8da 608-611 36c6f3f1734989cb 8a6246b8c05f144f 612-615 918d05dba93bf7af ae6b338ed65863e2 -616-619 cc1fc9fe234e6237 e04ec4070749160b +616-619 cc1fc9fe234e6237 7bd261119fc4428f 620-623 2b1c61c2f9ffecf7 8fbb465964e46b0e -624-627 9e25c994d6e9f497 2402434dc456687e +624-627 9e25c994d6e9f497 ff0f5b5dabbdf81d 628-631 9569acd0022c72e9 99eeb42b41eac287 632-635 02a74614410dc8de 82d095eaed3f21e1 636-639 0064b192beb2f16d fc9da6ebac16e671 640-643 b2176154f9609f29 3ff41983caa23faf 644-647 f865a601402d568c 943e9e9921ecb728 -648-651 8ddac4722587be4f ace2b06a8b8c8c2f +648-651 8ddac4722587be4f 719e1eacdf1a74b7 652-655 647ba94842de4a99 8fea29aad4870b17 656-659 22c5789b07c59689 0aaa723ae5b9df03 -660-663 f981a3e36eabb727 9f448a9a173571e3 +660-663 f981a3e36eabb727 84eb8d1c170cc1d6 664-667 6378568f624c46d3 cb9bf66dc3cda08a 668-671 53b118efd7818da9 4564ab4a7f0d559d 672-675 a3219e99fc477e55 c6df6f0d87b9b9c3 -676-679 2dbb5f9d13c34873 7580908dfdcfea1d +676-679 2dbb5f9d13c34873 899937bb2d2ad28d 680-683 f59699ffa5de8930 804313d49536b1eb 684-687 59cb168e9f9a08b3 398c4bfcc9646dad 688-691 6dfa1486645043d6 d81171f958bed162 692-695 bf91bdd85d72f097 a2515c0d5601d71b 696-699 30cef91da832598a 806297d879f04028 -700-703 9914c1d4b503c2fa b2d53e04d21b7bc6 +700-703 9914c1d4b503c2fa 1bbcb841dde6a8eb 704-707 db31d2e2f05ae765 1ab6149d78be4168 708-711 73b257c77137ec8d f77474faf5d33ba7 712-715 2723b37a94e6f61d cac8d4e20c22b5a2 @@ -189,14 +189,14 @@ 744-747 13ff170d3d8cf8cf 34c8f3d49bf9d824 748-751 2b646f2ebcca65e2 e1e2ef0f8acc106b 752-755 9171f34232dc9d5d 1f3743ef48f2b715 -756-759 6e922d3893444fd8 5e1314867d30fa3d +756-759 6e922d3893444fd8 b2e4c5edb8d864ad 760-763 d11f1fcb3df5ee76 33ce5e620fa20e08 764-767 b8a0235197ffeb8f 48ae6872a82dae8e 768-771 84d8643c18a5e371 df626fca46bed1f9 -772-775 dd10317147218f0a 224f4602080a058c -776-779 19a4e66646f42e84 84ab921e068c65f9 +772-775 dd10317147218f0a 05814afde8bd10d4 +776-779 19a4e66646f42e84 4bc8a478cb0b4b8a 780-783 0bb4a00c871b5a42 e910828f4f26627c -784-787 c0bb93b2e4e6cf4e aebb40e04549459e -788-791 850c2fdb73cd8f87 4bed71a422cb5f7a +784-787 c0bb93b2e4e6cf4e fdb7209055765194 +788-791 850c2fdb73cd8f87 833881782a09b19a 792-795 fc1f34260f230d80 02a005a2a4cc27c6 796-799 89400e26345a7cc1 71dc369195531335