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; }