From a70c5643a2215315cb5790ec883e0faeba304100 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 13:23:54 +0000 Subject: [PATCH 1/3] Start fix for #1481 Assisted-by: Claude Code:claude-opus-5-5 From 713e93ce025bf8f559d79b72fdb80a040f30586b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 13:44:29 +0000 Subject: [PATCH 2/3] Stop in-run VEX attesting npm file: copies A hosted npm scan with --vex attested a package not_affected when the same lock also installed that release from a file: tarball or a file: directory. npm installs those copies from their spec, so they stay unpatched, yet the run's own VEX said the app was fixed. The npm lock rewriter now records the patch for both shapes so the in-run VEX verifies the install instead of assuming the patch applied. A tarball copy already warned; a directory copy (a versionless link entry) is now matched through its source directory's entry and warns redirect_npm_link_entry_skipped. The registry copy is still pinned. Fixes #1481 Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/in_process_redirect.rs | 79 +++++++++++ .../src/patch/redirect/mod.rs | 126 +++++++++++++++++- 2 files changed, 202 insertions(+), 3 deletions(-) diff --git a/crates/socket-patch-cli/tests/in_process_redirect.rs b/crates/socket-patch-cli/tests/in_process_redirect.rs index d4f7dfe3c..9606a455e 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect.rs @@ -1613,6 +1613,85 @@ 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}" + ); + 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..cf3e41ec7 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -1382,6 +1382,21 @@ 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 directory's basename (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, + None => resolved.rsplit('/').next().unwrap_or(resolved), + }; + Some((name.to_string(), 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,11 +1424,20 @@ 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) .collect(); + // 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. + for target in targets.iter_mut().filter(|t| t.link && t.version.is_none()) { + if let Some((name, version)) = npm_link_source_identity(&lock, &target.pointer) { + target.name = name; + target.version = Some(version); + } + } // Entries npm installs from a git / url / `file:` spec: see // `vendor::npm_origin` (#326). let non_registry = npm_non_registry_entries(&lock, manifest_overrides); @@ -1448,11 +1472,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 +1532,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 +19326,92 @@ 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" + ); + } + } + /// #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. From 868fe6fca2be2700ec71cdadd65d234fcc39d7cf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 14:15:23 +0000 Subject: [PATCH 3/3] Match only file: links, keep scoped folder names Code review follow-ups. The link identity fill-in now applies only to a link npm reached through a file: spec, so a workspace member that happens to share a patched package's name and version is no longer reported as an unpatched copy. A source folder with no name field keeps its @scope/ parent, matching npm, so scoped file: directories are caught too. The npm rewrite golden is re-blessed: only its output digests change, for the seeded link and non-registry cases that now record the patch for the in-run VEX. Refs #1481 Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/in_process_redirect.rs | 2 + .../src/patch/redirect/mod.rs | 85 ++++++++++++++++--- .../tests/equivalence/npm_lock_rewrite.golden | 80 ++++++++--------- 3 files changed, 117 insertions(+), 50 deletions(-) diff --git a/crates/socket-patch-cli/tests/in_process_redirect.rs b/crates/socket-patch-cli/tests/in_process_redirect.rs index 9606a455e..076fa0485 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect.rs @@ -1681,6 +1681,8 @@ async fn scan_redirect_npm_file_copy_is_not_attested_in_run() { 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()) diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index cf3e41ec7..b3341e0ac 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -1384,17 +1384,25 @@ 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 directory's basename (npm omits -/// `name` when they agree). `None` when the lock records no version. +/// 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, - None => resolved.rsplit('/').next().unwrap_or(resolved), + 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.to_string(), version.to_string())) + Some((name, version.to_string())) } /// Rewrite a single npm lockfile (`package-lock.json` or `npm-shrinkwrap.json`) @@ -1429,18 +1437,22 @@ fn rewrite_one_npm_lock( .filter(NpmLockEntry::is_dependency) .map(NpmLockTarget::from) .collect(); + // 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. - for target in targets.iter_mut().filter(|t| t.link && t.version.is_none()) { + // 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 installs from a git / url / `file:` spec: see - // `vendor::npm_origin` (#326). - let non_registry = npm_non_registry_entries(&lock, manifest_overrides); // 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 @@ -19412,6 +19424,59 @@ mod tests { } } + /// #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