From cdff140a7c1224b8affa381e636e7fbee72a342f Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 20:12:14 -0400 Subject: [PATCH] langchain: 1 CWE-407 defect; forgejo: 2 new CWE-407 defects, MOADs 0002-0005 CLEAN langchain-0001: MultiVectorRetriever._get_relevant_documents() dedup IDs from vectorstore sub_docs uses list.contains() inside loop, O(k^2). k is unbounded in production RAG pipelines (configurable via search_kwargs). Fix: track seen IDs in a set, keep list for order. 499.5x at k=1000. forgejo-0002: LoadRepoConfig() license sort O(P*L) where L=776 licenses. Two SliceContainsString calls in back-to-back loops iterate full license list for each preferred license and vice versa. Fix: build lookup sets before loops. 19.5x at P=20 preferred licenses. forgejo-0003: synchronizePublicKeys() three O(N*M) scans per LDAP sync. Dedup of providedKeys is O(K^2), plus two O(P*G) set-difference loops. Runs per user per sync cycle. Fix: use maps for O(1) membership. 178.6x at K=G=500. forgejo-0001 (search.go RepoIDs) already patched in prior scan. MOADs 0002-0005 CLEAN for both targets. --- defects/forgejo-0002/patch/forgejo-0002.patch | 27 +++ .../test/ForgejoLicenseSortTest.class | Bin 0 -> 5402 bytes .../test/ForgejoLicenseSortTest.java | 157 +++++++++++++++ defects/forgejo-0003/patch/forgejo-0003.patch | 47 +++++ .../test/ForgejoSSHKeySyncTest.class | Bin 0 -> 4932 bytes .../test/ForgejoSSHKeySyncTest.java | 179 ++++++++++++++++++ .../langchain-0001/patch/langchain-0001.patch | 32 ++++ .../test/LangChainMultiVectorDedupTest.class | Bin 0 -> 5015 bytes .../test/LangChainMultiVectorDedupTest.java | 151 +++++++++++++++ 9 files changed, 593 insertions(+) create mode 100644 defects/forgejo-0002/patch/forgejo-0002.patch create mode 100644 defects/forgejo-0002/test/ForgejoLicenseSortTest.class create mode 100644 defects/forgejo-0002/test/ForgejoLicenseSortTest.java create mode 100644 defects/forgejo-0003/patch/forgejo-0003.patch create mode 100644 defects/forgejo-0003/test/ForgejoSSHKeySyncTest.class create mode 100644 defects/forgejo-0003/test/ForgejoSSHKeySyncTest.java create mode 100644 defects/langchain-0001/patch/langchain-0001.patch create mode 100644 defects/langchain-0001/test/LangChainMultiVectorDedupTest.class create mode 100644 defects/langchain-0001/test/LangChainMultiVectorDedupTest.java diff --git a/defects/forgejo-0002/patch/forgejo-0002.patch b/defects/forgejo-0002/patch/forgejo-0002.patch new file mode 100644 index 000000000..bcc6349c8 --- /dev/null +++ b/defects/forgejo-0002/patch/forgejo-0002.patch @@ -0,0 +1,27 @@ +--- a/modules/repository/init.go ++++ b/modules/repository/init.go +@@ -104,13 +104,16 @@ func LoadRepoConfig() error { + + // Filter out invalid names and promote preferred licenses. ++ // Build a set for O(1) lookups to avoid O(P*L) and O(L*P) scans. ++ licensesSet := make(map[string]struct{}, len(Licenses)) ++ for _, name := range Licenses { ++ licensesSet[strings.ToLower(name)] = struct{}{} ++ } ++ preferredSet := make(map[string]struct{}, len(setting.Repository.PreferredLicenses)) ++ for _, name := range setting.Repository.PreferredLicenses { ++ preferredSet[strings.ToLower(name)] = struct{}{} ++ } + sortedLicenses := make([]string, 0, len(Licenses)) + for _, name := range setting.Repository.PreferredLicenses { +- if util.SliceContainsString(Licenses, name, true) { ++ if _, ok := licensesSet[strings.ToLower(name)]; ok { + sortedLicenses = append(sortedLicenses, name) + } + } + for _, name := range Licenses { +- if !util.SliceContainsString(setting.Repository.PreferredLicenses, name, true) { ++ if _, ok := preferredSet[strings.ToLower(name)]; !ok { + sortedLicenses = append(sortedLicenses, name) + } + } diff --git a/defects/forgejo-0002/test/ForgejoLicenseSortTest.class b/defects/forgejo-0002/test/ForgejoLicenseSortTest.class new file mode 100644 index 0000000000000000000000000000000000000000..de7022bc8be39cb44c0201c71367eb63ae78e638 GIT binary patch literal 5402 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 sortLicensesDefective( + List allLicenses, List preferred) { + List sorted = new ArrayList<>(allLicenses.size()); + for (String name : preferred) { + if (containsIgnoreCase(allLicenses, name)) { + sorted.add(name); + } + } + for (String name : allLicenses) { + if (!containsIgnoreCase(preferred, name)) { + sorted.add(name); + } + } + return sorted; + } + + static boolean containsIgnoreCase(List list, String target) { + String t = target.toLowerCase(Locale.ROOT); + for (String s : list) { + if (s.toLowerCase(Locale.ROOT).equals(t)) return true; + } + return false; + } + + // Fixed: build sets first, then O(P + L) + static List sortLicensesFixed( + List allLicenses, List preferred) { + Set licenseSet = new HashSet<>(allLicenses.size() * 2); + for (String s : allLicenses) licenseSet.add(s.toLowerCase(Locale.ROOT)); + + Set preferredSet = new HashSet<>(preferred.size() * 2); + for (String s : preferred) preferredSet.add(s.toLowerCase(Locale.ROOT)); + + List sorted = new ArrayList<>(allLicenses.size()); + for (String name : preferred) { + if (licenseSet.contains(name.toLowerCase(Locale.ROOT))) { + sorted.add(name); + } + } + for (String name : allLicenses) { + if (!preferredSet.contains(name.toLowerCase(Locale.ROOT))) { + sorted.add(name); + } + } + return sorted; + } + + static long countDefectiveOps(List allLicenses, List preferred) { + long ops = 0; + // First loop: P * L + for (String name : preferred) { + ops += allLicenses.size(); + } + // Second loop: L * P + for (String name : allLicenses) { + ops += preferred.size(); + } + return ops; + } + + static long countFixedOps(List allLicenses, List preferred) { + // Build sets: O(L + P) + long ops = allLicenses.size() + preferred.size(); + // Two loops: O(P + L) + ops += preferred.size() + allLicenses.size(); + return ops; + } + + // Build a realistic license list like Forgejo's 776 licenses + static List makeLicenses(int count) { + List licenses = new ArrayList<>(count); + String[] templates = { + "MIT", "Apache-2.0", "GPL-2.0", "GPL-3.0", "LGPL-2.1", + "BSD-2-Clause", "BSD-3-Clause", "ISC", "MPL-2.0", "CDDL-1.0" + }; + for (int i = 0; i < count; i++) { + licenses.add(templates[i % templates.length] + "-variant-" + i); + } + return licenses; + } + + public static void main(String[] args) { + System.out.println("forgejo-0002: LoadRepoConfig license sort O(P*L) -> O(P+L)"); + System.out.println("=".repeat(60)); + + // Correctness test with small data + List licenses = Arrays.asList( + "MIT", "Apache-2.0", "GPL-2.0", "GPL-3.0", "BSD-2-Clause" + ); + List preferred = Arrays.asList("GPL-3.0", "MIT"); + + List defective = sortLicensesDefective(licenses, preferred); + List fixed = sortLicensesFixed(licenses, preferred); + + // Both should start with preferred items (in preferred order), rest follow + if (!defective.get(0).equals("GPL-3.0") || !defective.get(1).equals("MIT")) { + throw new AssertionError("Defective sort wrong ordering: " + defective); + } + if (!fixed.get(0).equals("GPL-3.0") || !fixed.get(1).equals("MIT")) { + throw new AssertionError("Fixed sort wrong ordering: " + fixed); + } + if (defective.size() != fixed.size()) { + throw new AssertionError("Size mismatch: " + defective.size() + " vs " + fixed.size()); + } + System.out.println("PASS: correctness verified"); + + // Benchmark: 776 licenses (realistic), various preferred counts + System.out.printf("%n%-10s %15s %12s %10s%n", + "licenses", "defective_ops", "fixed_ops", "ratio"); + System.out.println("-".repeat(48)); + + int L = 776; + List allLicenses = makeLicenses(L); + int[] prefCounts = {1, 5, 10, 20}; + for (int P : prefCounts) { + List pref = allLicenses.subList(0, P); + long def = countDefectiveOps(allLicenses, pref); + long fix = countFixedOps(allLicenses, pref); + double ratio = (double) def / fix; + System.out.printf("L=%-4d P=%-4d %15d %12d %9.1fx%n", L, P, def, fix, ratio); + } + + long def776 = countDefectiveOps(allLicenses, allLicenses.subList(0, 5)); + long fix776 = countFixedOps(allLicenses, allLicenses.subList(0, 5)); + double ratio776 = (double) def776 / fix776; + + if (ratio776 < 4) { + throw new AssertionError("Expected >4x ratio at L=776, P=5, got " + ratio776); + } + + System.out.println("\nAll assertions PASS"); + System.out.println("Fix: build licensesSet and preferredSet before loops for O(1) lookup"); + } +} diff --git a/defects/forgejo-0003/patch/forgejo-0003.patch b/defects/forgejo-0003/patch/forgejo-0003.patch new file mode 100644 index 000000000..0c4f8b443 --- /dev/null +++ b/defects/forgejo-0003/patch/forgejo-0003.patch @@ -0,0 +1,47 @@ +--- a/models/asymkey/ssh_key.go ++++ b/models/asymkey/ssh_key.go +@@ -378,19 +378,22 @@ func synchronizePublicKeys(ctx context.Context, s *auth.Source, usr *user_model. + // Process the provided keys to remove duplicates and name part +- var providedKeys []string ++ providedKeysSet := make(map[string]struct{}) ++ var providedKeys []string + for _, v := range sshPublicKeys { + sshKeySplit := strings.Split(v, " ") + if len(sshKeySplit) > 1 { + key := strings.Join(sshKeySplit[:2], " ") +- if !util.SliceContainsString(providedKeys, key) { ++ if _, exists := providedKeysSet[key]; !exists { ++ providedKeysSet[key] = struct{}{} + providedKeys = append(providedKeys, key) + } + } + } + + // Check if Public Key sync is needed +@@ -399,14 +402,16 @@ func synchronizePublicKeys(ctx context.Context, s *auth.Source, usr *user_model. + + // Add new Public SSH Keys that doesn't already exist in DB ++ giteaKeysSet := make(map[string]struct{}, len(giteaKeys)) ++ for _, k := range giteaKeys { ++ giteaKeysSet[k] = struct{}{} ++ } + var newKeys []string + for _, key := range providedKeys { +- if !util.SliceContainsString(giteaKeys, key) { ++ if _, exists := giteaKeysSet[key]; !exists { + newKeys = append(newKeys, key) + } + } + if AddPublicKeysBySource(ctx, usr, s, newKeys) { + sshKeysNeedUpdate = true + } + + // Mark keys from DB that no longer exist in the source for deletion + var giteaKeysToDelete []string + for _, giteaKey := range giteaKeys { +- if !util.SliceContainsString(providedKeys, giteaKey) { ++ if _, exists := providedKeysSet[giteaKey]; !exists { + log.Trace("synchronizePublicKeys[%s]: Marking Public SSH Key for deletion for user %s: %v", s.Name, usr.Name, giteaKey) + giteaKeysToDelete = append(giteaKeysToDelete, giteaKey) + } + } diff --git a/defects/forgejo-0003/test/ForgejoSSHKeySyncTest.class b/defects/forgejo-0003/test/ForgejoSSHKeySyncTest.class new file mode 100644 index 0000000000000000000000000000000000000000..8879062cdc27c8247a6a0d34cd4cfc2ab07b3418 GIT binary patch literal 4932 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 MYWx--vQCHo17^qMdH?_b literal 0 HcmV?d00001 diff --git a/defects/forgejo-0003/test/ForgejoSSHKeySyncTest.java b/defects/forgejo-0003/test/ForgejoSSHKeySyncTest.java new file mode 100644 index 000000000..cdb96e6aa --- /dev/null +++ b/defects/forgejo-0003/test/ForgejoSSHKeySyncTest.java @@ -0,0 +1,179 @@ +import java.util.*; + +/** + * Unit test simulating Forgejo forgejo-0003: + * models/asymkey/ssh_key.go synchronizePublicKeys() has three O(N*M) scans: + * + * 1. Building providedKeys with dedup: O(K^2) where K = LDAP-provided key count + * for _, v := range sshPublicKeys { + * key = ... + * if !util.SliceContainsString(providedKeys, key) { // O(|providedKeys|) each time + * + * 2. Finding new keys: O(P * G) where P = provided, G = DB keys + * for _, key := range providedKeys { + * if !util.SliceContainsString(giteaKeys, key) { // O(G) each + * + * 3. Finding deleted keys: O(G * P) + * for _, giteaKey := range giteaKeys { + * if !util.SliceContainsString(providedKeys, giteaKey) { // O(P) each + * + * This runs per user per LDAP sync cycle. With many users or large key sets it accumulates. + * Fix: build maps for O(1) lookup before each loop. + */ +public class ForgejoSSHKeySyncTest { + + // Defective: O(K^2) dedup + O(P*G) + O(G*P) + static long countDefectiveOps(int rawKeyCount, int giteaKeyCount) { + // Phase 1: dedup raw keys into providedKeys + // Assume half are unique: K/2 unique keys, K/2 duplicates + // Each insertion checks against growing list + long ops = 0; + int provided = 0; + for (int i = 0; i < rawKeyCount; i++) { + ops += provided; // SliceContainsString cost + if (i % 2 == 0) provided++; // ~half unique + } + // Phase 2: find new keys (provided not in gitea) + ops += (long) provided * giteaKeyCount; + // Phase 3: find keys to delete (gitea not in provided) + ops += (long) giteaKeyCount * provided; + return ops; + } + + static long countFixedOps(int rawKeyCount, int giteaKeyCount) { + // Phase 1: dedup with set — O(K) total + long ops = rawKeyCount; + int provided = rawKeyCount / 2; // ~half unique + // Build giteaKeysSet: O(G) + ops += giteaKeyCount; + // Phase 2: find new keys — O(P) + ops += provided; + // Phase 3: find keys to delete — O(G) + ops += giteaKeyCount; + return ops; + } + + // Simulate the sync logic for correctness + static Map> syncKeysDefective( + List rawKeys, List giteaKeys) { + // Dedup rawKeys + List provided = new ArrayList<>(); + for (String k : rawKeys) { + if (!provided.contains(k)) { + provided.add(k); + } + } + // Find new keys + List toAdd = new ArrayList<>(); + for (String k : provided) { + if (!giteaKeys.contains(k)) { + toAdd.add(k); + } + } + // Find keys to delete + List toDelete = new ArrayList<>(); + for (String k : giteaKeys) { + if (!provided.contains(k)) { + toDelete.add(k); + } + } + Map> result = new HashMap<>(); + result.put("toAdd", toAdd); + result.put("toDelete", toDelete); + return result; + } + + static Map> syncKeysFixed( + List rawKeys, List giteaKeys) { + // Dedup rawKeys with set + Set providedSet = new LinkedHashSet<>(); + List provided = new ArrayList<>(); + for (String k : rawKeys) { + if (providedSet.add(k)) { + provided.add(k); + } + } + // Build giteaKeys set + Set giteaSet = new HashSet<>(giteaKeys); + // Find new keys + List toAdd = new ArrayList<>(); + for (String k : provided) { + if (!giteaSet.contains(k)) { + toAdd.add(k); + } + } + // Find keys to delete + List toDelete = new ArrayList<>(); + for (String k : giteaKeys) { + if (!providedSet.contains(k)) { + toDelete.add(k); + } + } + Map> result = new HashMap<>(); + result.put("toAdd", toAdd); + result.put("toDelete", toDelete); + return result; + } + + public static void main(String[] args) { + System.out.println("forgejo-0003: synchronizePublicKeys() O(K^2+P*G) -> O(K+P+G)"); + System.out.println("=".repeat(60)); + + // Correctness test + List rawKeys = Arrays.asList( + "ssh-rsa AAAA1", "ssh-rsa AAAA2", "ssh-rsa AAAA1", // dup + "ssh-rsa AAAA3", "ssh-rsa AAAA2" // more dups + ); + List giteaKeys = Arrays.asList( + "ssh-rsa AAAA2", "ssh-rsa AAAA4" + ); + + Map> def = syncKeysDefective(rawKeys, giteaKeys); + Map> fix = syncKeysFixed(rawKeys, giteaKeys); + + // Expected: add AAAA1 and AAAA3 (new from LDAP), delete AAAA4 (no longer in LDAP) + List expectedAdd = Arrays.asList("ssh-rsa AAAA1", "ssh-rsa AAAA3"); + List expectedDelete = Arrays.asList("ssh-rsa AAAA4"); + + if (!def.get("toAdd").equals(expectedAdd)) { + throw new AssertionError("Defective toAdd wrong: " + def.get("toAdd")); + } + if (!def.get("toDelete").equals(expectedDelete)) { + throw new AssertionError("Defective toDelete wrong: " + def.get("toDelete")); + } + if (!fix.get("toAdd").equals(expectedAdd)) { + throw new AssertionError("Fixed toAdd wrong: " + fix.get("toAdd")); + } + if (!fix.get("toDelete").equals(expectedDelete)) { + throw new AssertionError("Fixed toDelete wrong: " + fix.get("toDelete")); + } + System.out.println("PASS: correctness verified"); + + // Performance comparison + System.out.printf("%n%-20s %15s %12s %10s%n", + "scenario", "defective_ops", "fixed_ops", "ratio"); + System.out.println("-".repeat(58)); + + int[][] scenarios = { + {10, 10}, {50, 50}, {100, 100}, {500, 500}, {1000, 1000} + }; + for (int[] sc : scenarios) { + int K = sc[0], G = sc[1]; + long dOps = countDefectiveOps(K, G); + long fOps = countFixedOps(K, G); + double ratio = (double) dOps / fOps; + System.out.printf("K=%-4d G=%-4d %15d %12d %9.1fx%n", + K, G, dOps, fOps, ratio); + } + + long dOps = countDefectiveOps(500, 500); + long fOps = countFixedOps(500, 500); + double ratio = (double) dOps / fOps; + if (ratio < 10) { + throw new AssertionError("Expected >10x ratio at K=500 G=500, got " + ratio); + } + + System.out.println("\nAll assertions PASS"); + System.out.println("Fix: use providedKeysSet(map) for O(1) dedup; giteaKeysSet for O(1) diff"); + } +} diff --git a/defects/langchain-0001/patch/langchain-0001.patch b/defects/langchain-0001/patch/langchain-0001.patch new file mode 100644 index 000000000..b05a70c0c --- /dev/null +++ b/defects/langchain-0001/patch/langchain-0001.patch @@ -0,0 +1,32 @@ +--- a/libs/langchain/langchain_classic/retrievers/multi_vector.py ++++ b/libs/langchain/langchain_classic/retrievers/multi_vector.py +@@ -105,9 +105,10 @@ class MultiVectorRetriever(BaseRetriever): + sub_docs = self.vectorstore.similarity_search(query, **self.search_kwargs) + + # We do this to maintain the order of the IDs that are returned +- ids = [] ++ seen_ids: set = set() ++ ids = [] + for d in sub_docs: +- if self.id_key in d.metadata and d.metadata[self.id_key] not in ids: ++ if self.id_key in d.metadata and d.metadata[self.id_key] not in seen_ids: ++ seen_ids.add(d.metadata[self.id_key]) + ids.append(d.metadata[self.id_key]) + docs = self.docstore.mget(ids) + return [d for d in docs if d is not None] +@@ -147,9 +148,10 @@ class MultiVectorRetriever(BaseRetriever): + sub_docs = await self.vectorstore.asimilarity_search( + query, **self.search_kwargs + ) + + # We do this to maintain the order of the IDs that are returned +- ids = [] ++ seen_ids_async: set = set() ++ ids = [] + for d in sub_docs: +- if self.id_key in d.metadata and d.metadata[self.id_key] not in ids: ++ if self.id_key in d.metadata and d.metadata[self.id_key] not in seen_ids_async: ++ seen_ids_async.add(d.metadata[self.id_key]) + ids.append(d.metadata[self.id_key]) + docs = await self.docstore.amget(ids) + return [d for d in docs if d is not None] diff --git a/defects/langchain-0001/test/LangChainMultiVectorDedupTest.class b/defects/langchain-0001/test/LangChainMultiVectorDedupTest.class new file mode 100644 index 0000000000000000000000000000000000000000..4a43070eb4d8c366c63f7e6df847b6e4a0eeddc8 GIT binary patch literal 5015 zcmcIod30P=9scgjWZs)4WN9WOUEsBJXwqbow3M`&X$xu6+LWY$PE8F3%1h=onPxI? zoTW_x0kv!esj`WPELGvSfWXlvZK1f~f{44Ih`WILuji%oK zp|Q6#-Q$eM0;oc@f(1d;AS6&dS42P=)pOg839d&(mrISagQ!EjfG2GY*A&;DW=BlH zq8aAJ^Vwv2L?T`i#5p*Z=!VU7UQecT0-**Q-1G(HRsk#xVwn_n>xqPd&D=662&?X=M;iXeQ`f@*2akrAI-FKvhZd%u*5Qqg6%x z7V|swbiy1jqZj4hN$$9S8UkUI3OLeP7ZX)fgU-~hFB2mv4IpyqI%M#P)_w! zbA^k7xEP(ZXIvjMy3F*jp5K+skFuA`>*>5jN{{0+ix!)}m=9g(Rp*?K97&r{pb$73pb1lCLO;Zaf_X=`g+71jC*seE#P(Pw6N81$2| z-^gk`-CDv(6f#N1$%=S!KlEJUUJhVxtT^vx8z}`Ayeusixa82B{^Xv z1eQ6WYhkcfoStZBw%8ZMm|S1A^qr6#4*QG6Hp0I9d+v%;k)1K53>m zXR~Hjl9df22cl5+>#2g#zgOV=`7&pk*$(B^5ZEsx?!YXSqD$tg_+jFp4CrfSdC@LZ zvBrUF*0TL7J|xg^X=gkh)rQRcsFul^iNdg<4V&34LrOEV2_tJH>|j!H14BNX4oBAI zwD79cXC7_2a9YKO85LtHK1!ZaqNoj6mT|ydLd~~#|+B*?W`|&9}py1Qerwpic zHWgC@;qOePG`*NtIZei`iW34Gna)v-H7KPsc2^`5IcOlWB{uO-v$dX)#VLUfO-t^bwop+hdPcm3r&~iZ(I~gewdk3Q zk+$A>UUZoW`lfXx^BX9^Lyj_38^u|7RhoQo$%(t5s-rRKKT;ktfq zs&9an>{8+kl*%$A#X;rVqv<1FnmfTx#* zViDwZ>xayqd8QQnh{w-v9-jWVS;!6>Taq$mmi4~ZY0)A}Ia6(uY36hJte)vJ@}p*g zRpI9>aCU%4goM_^ZGFBZg@S;Bs^xD}>06fSx2eFp3g0E3JJ`xwZ}&-@M?B$P2UnksYdd$g zaD~~iaYa2%^`7H6-$k^+X4%y&Xs^4 zx;D_4v?sWn&EYaa7cK#iV6gk-k%&I9$_8BqNWNU}-qzCI0^P2pOuzmCW{h%8EcF*9m5Iup7 zfbGyj9`X3;HzFmU+Tqs4PpxhS+-3&-7813YA-A;zhW>0LOX6)TcEEgxV7`m*IbgzU zWd`|H(n%VPE@g}SbOw4U-sh-(pMBoiIsSjioXfcU4hChMyzZnK1I(OVjw$bwIX=9I zA26$l$Gw{|Ud1?C{|q)&Z#{->vc0r&{W0v|OQrH077n_r-ya|JRPTxpmdT#CdN4ky z$XB&`cYLtY%fTsJ**iy;VqQvcuCHkd!`t~tMm-VMhLkJ_H78*XMyBw=6DRmw4^A-UJh@ng7kmlV9Cve~vkebztduJrd0 zdMa1<50+IX`UkyL;vfBkN|pPq{%Bc!S=3waJ=Lzb+trX7QjX%`dT&T=Y4^$K3i%jb zQNO!A5DJ8RM^RVr4+T!(&NJZ^V0gV*@1MevwE@}Oy|%nIaJaVou-Q=|aolGE4f&>U zKe5&ZrtlfUtu41&Z(YR{j#{8YzRJX-SW;I}gU9cG(B>xOtE)JHFWBZEQn}UDm5Fw5 zWlg&h@`jWtd^xJtt8#CjZ0=qgsP!G@_GU-9-1{ljA}Qpx_m&F8P*zJU#Nc(XD0ReQ zW2h^y!86>vT9RK^ega=DF+b@rf81$LI_-08W%S#H=pp{J0qkZ-UcpknhgEWjpTY?Y z!$6W*kYgSk!af{f?S2&Fv@OF}%;FW~@lO;)6+itJV?xCEJ+}$hiXL1icHnw(18x*Y z_~G>kWgf%L;z`^hCUL7ch1@-31 zhpic_ShAGzc-$nyjUQXo`~*K`dp-|<-!bieW<7slJzue&zp|dMS dedupList(List subDocIds) { + List ids = new ArrayList<>(); + for (String id : subDocIds) { + if (!ids.contains(id)) { // O(|ids|) scan + ids.add(id); + } + } + return ids; + } + + // Fixed: O(k) set-assisted dedup, preserving order + static List dedupSet(List subDocIds) { + Set seen = new LinkedHashSet<>(); + List ids = new ArrayList<>(); + for (String id : subDocIds) { + if (seen.add(id)) { // O(1) amortized + ids.add(id); + } + } + return ids; + } + + // Measure operation count for list-based dedup + static long countListOps(List subDocIds) { + List ids = new ArrayList<>(); + long ops = 0; + for (String id : subDocIds) { + ops += ids.size(); // each contains() scans whole list + if (!ids.contains(id)) { + ids.add(id); + } + } + return ops; + } + + // Measure operation count for set-based dedup + static long countSetOps(List subDocIds) { + Set seen = new HashSet<>(); + long ops = 0; + for (String id : subDocIds) { + ops += 1; // O(1) hash lookup + seen.add(id); + } + return ops; + } + + // Simulate sub_docs where each doc has an id pointing to a parent document + // k sub-docs may map to fewer unique parent ids (many-to-one) + static List makeSubDocIds(int k, int uniqueParents) { + Random rng = new Random(42); + List ids = new ArrayList<>(k); + for (int i = 0; i < k; i++) { + ids.add("parent-" + (rng.nextInt(uniqueParents))); + } + return ids; + } + + static void append(List list, String val) { + list.add(val); + } + + // Override dedupList to use add not append + static List dedupListFixed(List subDocIds) { + List ids = new ArrayList<>(); + for (String id : subDocIds) { + if (!ids.contains(id)) { + ids.add(id); + } + } + return ids; + } + + public static void main(String[] args) { + System.out.println("langchain-0001: MultiVectorRetriever ID dedup O(k^2) -> O(k)"); + System.out.println("=".repeat(60)); + + // Test correctness + List input = Arrays.asList( + "p1", "p2", "p1", "p3", "p2", "p4", "p1" + ); + List expected = Arrays.asList("p1", "p2", "p3", "p4"); + + List listResult = dedupListFixed(input); + List setResult = dedupSet(input); + + if (!listResult.equals(expected)) { + throw new AssertionError("List dedup wrong: " + listResult); + } + if (!setResult.equals(expected)) { + throw new AssertionError("Set dedup wrong: " + setResult); + } + System.out.println("PASS: both produce correct ordered dedup"); + + // Benchmark comparison at various k values + System.out.printf("%n%-8s %12s %12s %10s%n", + "k", "list_ops", "set_ops", "ratio"); + System.out.println("-".repeat(44)); + + int[] kValues = {10, 50, 100, 500, 1000}; + for (int k : kValues) { + // Worst case: all unique ids (no duplicates) maximizes list scan ops + List allUnique = new ArrayList<>(k); + for (int i = 0; i < k; i++) allUnique.add("p" + i); + + long listOps = countListOps(allUnique); + long setOps = countSetOps(allUnique); + double ratio = (double) listOps / setOps; + + System.out.printf("%-8d %12d %12d %9.1fx%n", k, listOps, setOps, ratio); + + if (k >= 100 && ratio < 40) { + throw new AssertionError("Expected >40x ratio at k=" + k + ", got " + ratio); + } + } + + // Realistic scenario: k=100 sub-docs mapping to 20 unique parents + int k = 100, parents = 20; + List realistic = makeSubDocIds(k, parents); + long listOpsR = countListOps(realistic); + long setOpsR = countSetOps(realistic); + double ratioR = (double) listOpsR / setOpsR; + System.out.printf("%nRealistic k=%d, %d parents: list=%d set=%d ratio=%.1fx%n", + k, parents, listOpsR, setOpsR, ratioR); + + if (ratioR < 5) { + throw new AssertionError("Expected >5x ratio in realistic case, got " + ratioR); + } + + System.out.println("\nAll assertions PASS"); + System.out.println("Fix: replace 'ids = []; id not in ids' with set-assisted dedup"); + System.out.println(" seen_ids = set()"); + System.out.println(" if id not in seen_ids: seen_ids.add(id); ids.append(id)"); + } +}