From 081cb888ec93b42f186b8b50a039b46bd22676d8 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Mon, 24 Aug 2026 16:18:28 -0700 Subject: [PATCH] Keep a share link's own item however long its name runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `shareDeepLink` now builds through `sharedViewLink`, whose length budget applied to the first item too: a parameter that alone overran it was dropped, and the link came out as a bare `?shared=`. A name of a few hundred characters — the GUI allows five hundred, and encoding triples every non-ASCII one — was enough, so that item's own link in the mail landed on Shared with nothing picked out, where it used to open the item. The first item now always goes in; the budget only decides how many more join it. One long link is still the item the mail is about, and it is no worse than a bare origin for the clients that truncate it. --- .../services/share/shareDeepLink.test.ts | 22 +++++++++++++++++++ src/backend/services/share/shareDeepLink.ts | 10 +++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/backend/services/share/shareDeepLink.test.ts b/src/backend/services/share/shareDeepLink.test.ts index c5693c4a0..fe590143a 100644 --- a/src/backend/services/share/shareDeepLink.test.ts +++ b/src/backend/services/share/shareDeepLink.test.ts @@ -121,6 +121,28 @@ describe('sharedViewLink', () => { ); }); + // A name can run to hundreds of characters, and encoding multiplies + // non-ASCII ones; the link exists to name the item, so it always does, + // however long — the length cap only limits how many more join it. + it('keeps the first item even when it alone outgrows the length cap', () => { + const long = `/alice/${UID}/${'\u5831\u544a'.repeat(120)}.pdf`; + const short = `/alice/${UID}/a.txt`; + expect(shareDeepLink('https://puter.com', long).length).toBeGreaterThan( + SHARE_DEEP_LINK_MAX_LENGTH, + ); + expect( + new URL( + shareDeepLink('https://puter.com', long), + ).searchParams.getAll('shared'), + ).toEqual([long]); + // Nothing fits after it, and nothing later is taken instead. + expect( + new URL( + sharedViewLink('https://puter.com', [long, short]), + ).searchParams.getAll('shared'), + ).toEqual([long]); + }); + it('names an item once however often it was queued', () => { const path = `/alice/${UID}/a.txt`; expect(sharedViewLink('https://puter.com', [path, path])).toBe( diff --git a/src/backend/services/share/shareDeepLink.ts b/src/backend/services/share/shareDeepLink.ts index ffa77aa0b..193cb3be4 100644 --- a/src/backend/services/share/shareDeepLink.ts +++ b/src/backend/services/share/shareDeepLink.ts @@ -62,9 +62,10 @@ export const SHARE_DEEP_LINK_ITEMS_LIMIT = 20; /** * How long a link may run, in characters. Somewhere past two thousand, older * mail clients cut a URL off or stop making it clickable — and this is the - * button — so items are added only while the whole link stays within this. A - * single item always fits: a username, a uuid and a filename come to a few - * hundred, and it takes long names for the count above to matter first. + * button — so items are added only while the whole link stays within this. The + * first item goes in regardless: a link that names nothing is no better than + * the origin, and one long name (hundreds of characters, tripled by encoding + * when non-ASCII) is still the item the mail is about. */ export const SHARE_DEEP_LINK_MAX_LENGTH = 2000; @@ -84,7 +85,8 @@ export const sharedViewLink = (origin: string, paths: string[]): string => { if (params.length === SHARE_DEEP_LINK_ITEMS_LIMIT) break; const param = `${SHARE_DEEP_LINK_PARAM}=${encodeURIComponent(path)}`; const added = param.length + (params.length === 0 ? 0 : '&'.length); - if (length + added > SHARE_DEEP_LINK_MAX_LENGTH) break; + const overLength = length + added > SHARE_DEEP_LINK_MAX_LENGTH; + if (params.length > 0 && overLength) break; params.push(param); length += added; }