From bb904d113048330f0c3f53dc1314a1c2391f6d51 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 5 Aug 2026 13:12:58 -0300 Subject: [PATCH 1/3] fix: Sanitizes filename on iOS file share --- packages/mobile/package.json | 1 + packages/mobile/src/Lib/MobileDevice.ts | 4 +++- .../utils/src/Domain/FileName/FileNameUtils.ts | 5 +++++ .../Components/FilePreview/PreviewComponent.tsx | 7 +------ .../NativeMobileWeb/DownloadBlobOnAndroid.tsx | 14 ++++++-------- yarn.lock | 1 + 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 870a72a3f7a..abc60f9b921 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -42,6 +42,7 @@ "@standardnotes/config": "^2.4.3", "@standardnotes/react-native-utils": "1.0.1", "@standardnotes/snjs": "workspace:*", + "@standardnotes/utils": "workspace:*", "@standardnotes/web": "workspace:*", "@tsconfig/react-native": "^3.0.2", "@types/react": "^19.1.2", diff --git a/packages/mobile/src/Lib/MobileDevice.ts b/packages/mobile/src/Lib/MobileDevice.ts index 29d9c75a23f..329bf68a18b 100644 --- a/packages/mobile/src/Lib/MobileDevice.ts +++ b/packages/mobile/src/Lib/MobileDevice.ts @@ -1,4 +1,5 @@ import SNReactNative from '@standardnotes/react-native-utils' +import { sanitizeFileNameForNativeWrite } from '@standardnotes/utils' import { AppleIAPProductId, AppleIAPReceipt, @@ -502,7 +503,8 @@ export class MobileDevice implements MobileDeviceInterface { directory = saveInTempLocation ? CachesDirectoryPath : DownloadDirectoryPath } - return `${directory}/${filename}` + const safeFilename = sanitizeFileNameForNativeWrite(filename) + return `${directory}/${safeFilename}` } async downloadBase64AsFile( diff --git a/packages/utils/src/Domain/FileName/FileNameUtils.ts b/packages/utils/src/Domain/FileName/FileNameUtils.ts index 8d849d5ddeb..dfa036172ee 100644 --- a/packages/utils/src/Domain/FileName/FileNameUtils.ts +++ b/packages/utils/src/Domain/FileName/FileNameUtils.ts @@ -14,6 +14,11 @@ export function sanitizeFileName(name: string): string { return name.trim().replace(/[.\\/:"?*|<>]/g, '_') } +export function sanitizeFileNameForNativeWrite(filename: string): string { + const { name, ext } = parseFileName(filename) + return `${sanitizeFileName(name)}.${ext}` +} + export function truncateFileName(name: string, maxLength: number): string { return name.length > maxLength ? name.slice(0, maxLength) : name } diff --git a/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx b/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx index 622678c0160..1417ed29f70 100644 --- a/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx @@ -9,7 +9,6 @@ import ImagePreview from './ImagePreview' import { OptionalSuperEmbeddedImageProps } from './OptionalSuperEmbeddedImageProps' import { PreviewableTextFileTypes, RequiresNativeFilePreview } from './isFilePreviewable' import TextPreview from './TextPreview' -import { parseFileName, sanitizeFileName } from '@standardnotes/utils' import VideoPreview from './VideoPreview' type Props = { @@ -60,11 +59,7 @@ const PreviewComponent: FunctionComponent = ({ }), ) - const { name, ext } = parseFileName(file.name) - const sanitizedName = sanitizeFileName(name) - const filename = `${sanitizedName}.${ext}` - - void application.mobileDevice.previewFile(fileBase64, filename) + void application.mobileDevice.previewFile(fileBase64, file.name) }, [application, bytes, file.mimeType, file.name, isNativeMobileWeb]) if (isNativeMobileWeb && requiresNativePreview) { diff --git a/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx b/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx index 2502965410b..6c554f4c706 100644 --- a/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx +++ b/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx @@ -1,6 +1,6 @@ import { getBase64FromBlob } from '@/Utils' -import { parseFileName, sanitizeFileName } from '@standardnotes/utils' import { MobileDeviceInterface } from '@standardnotes/snjs' +import { sanitizeFileNameForNativeWrite } from '@standardnotes/utils' import { addToast, ToastType, dismissToast } from '@standardnotes/toast' import { c } from 'ttag' @@ -12,18 +12,16 @@ export const downloadBlobOnAndroid = async ( filename: string, showToast = true, ) => { + const safeFilename = sanitizeFileNameForNativeWrite(filename) let loadingToastId: string | undefined if (showToast) { loadingToastId = addToast({ type: ToastType.Loading, - message: jtString(c('B8.MobileDesktopShared.Mobile.Info').jt`Downloading ${filename}..`), + message: jtString(c('B8.MobileDesktopShared.Mobile.Info').jt`Downloading ${safeFilename}..`), }) } const base64 = await getBase64FromBlob(blob) - const { name, ext } = parseFileName(filename) - const sanitizedName = sanitizeFileName(name) - filename = `${sanitizedName}.${ext}` - const downloaded = await mobileDevice.downloadBase64AsFile(base64, filename) + const downloaded = await mobileDevice.downloadBase64AsFile(base64, safeFilename) if (loadingToastId) { dismissToast(loadingToastId) } @@ -33,12 +31,12 @@ export const downloadBlobOnAndroid = async ( if (downloaded) { addToast({ type: ToastType.Success, - message: jtString(c('B8.MobileDesktopShared.Mobile.Info').jt`Downloaded ${filename}`), + message: jtString(c('B8.MobileDesktopShared.Mobile.Info').jt`Downloaded ${safeFilename}`), }) } else { addToast({ type: ToastType.Error, - message: jtString(c('B8.MobileDesktopShared.Mobile.Error').jt`Could not download ${filename}`), + message: jtString(c('B8.MobileDesktopShared.Mobile.Error').jt`Could not download ${safeFilename}`), }) } } diff --git a/yarn.lock b/yarn.lock index 705f5286875..1f63f8302d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10001,6 +10001,7 @@ __metadata: "@standardnotes/config": ^2.4.3 "@standardnotes/react-native-utils": 1.0.1 "@standardnotes/snjs": "workspace:*" + "@standardnotes/utils": "workspace:*" "@standardnotes/web": "workspace:*" "@tsconfig/react-native": ^3.0.2 "@types/react": ^19.1.2 From 4301aee90da23f28c1525d4706544d6a4a51a2ff Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Fri, 7 Aug 2026 14:49:08 -0300 Subject: [PATCH 2/3] fix: move util to mobile package --- packages/mobile/package.json | 1 - packages/mobile/src/Lib/MobileDevice.ts | 2 +- packages/mobile/src/Lib/Utils.ts | 21 +++++++++++++++++++ .../src/Domain/FileName/FileNameUtils.ts | 5 ----- .../NativeMobileWeb/DownloadBlobOnAndroid.tsx | 14 +++++++------ 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/packages/mobile/package.json b/packages/mobile/package.json index abc60f9b921..870a72a3f7a 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -42,7 +42,6 @@ "@standardnotes/config": "^2.4.3", "@standardnotes/react-native-utils": "1.0.1", "@standardnotes/snjs": "workspace:*", - "@standardnotes/utils": "workspace:*", "@standardnotes/web": "workspace:*", "@tsconfig/react-native": "^3.0.2", "@types/react": "^19.1.2", diff --git a/packages/mobile/src/Lib/MobileDevice.ts b/packages/mobile/src/Lib/MobileDevice.ts index 329bf68a18b..988201f7cd5 100644 --- a/packages/mobile/src/Lib/MobileDevice.ts +++ b/packages/mobile/src/Lib/MobileDevice.ts @@ -1,5 +1,5 @@ import SNReactNative from '@standardnotes/react-native-utils' -import { sanitizeFileNameForNativeWrite } from '@standardnotes/utils' +import { sanitizeFileNameForNativeWrite } from './Utils' import { AppleIAPProductId, AppleIAPReceipt, diff --git a/packages/mobile/src/Lib/Utils.ts b/packages/mobile/src/Lib/Utils.ts index 193913d9711..6eda635c133 100644 --- a/packages/mobile/src/Lib/Utils.ts +++ b/packages/mobile/src/Lib/Utils.ts @@ -38,3 +38,24 @@ export function isSameDay(dateA: Date, dateB: Date) { dateA.getDate() === dateB.getDate() ) } + +function parseFileName(fileName: string): { + name: string + ext: string +} { + const pattern = /(?:\.([^.]+))$/ + const extMatches = pattern.exec(fileName) + const ext = extMatches?.[1] || '' + const name = fileName.includes('.') ? fileName.substring(0, fileName.lastIndexOf('.')) : fileName + + return { name, ext } +} + +function sanitizeFileName(name: string): string { + return name.trim().replace(/[.\\/:"?*|<>]/g, '_') +} + +export function sanitizeFileNameForNativeWrite(filename: string): string { + const { name, ext } = parseFileName(filename) + return `${sanitizeFileName(name)}.${ext}` +} diff --git a/packages/utils/src/Domain/FileName/FileNameUtils.ts b/packages/utils/src/Domain/FileName/FileNameUtils.ts index dfa036172ee..8d849d5ddeb 100644 --- a/packages/utils/src/Domain/FileName/FileNameUtils.ts +++ b/packages/utils/src/Domain/FileName/FileNameUtils.ts @@ -14,11 +14,6 @@ export function sanitizeFileName(name: string): string { return name.trim().replace(/[.\\/:"?*|<>]/g, '_') } -export function sanitizeFileNameForNativeWrite(filename: string): string { - const { name, ext } = parseFileName(filename) - return `${sanitizeFileName(name)}.${ext}` -} - export function truncateFileName(name: string, maxLength: number): string { return name.length > maxLength ? name.slice(0, maxLength) : name } diff --git a/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx b/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx index 6c554f4c706..2502965410b 100644 --- a/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx +++ b/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx @@ -1,6 +1,6 @@ import { getBase64FromBlob } from '@/Utils' +import { parseFileName, sanitizeFileName } from '@standardnotes/utils' import { MobileDeviceInterface } from '@standardnotes/snjs' -import { sanitizeFileNameForNativeWrite } from '@standardnotes/utils' import { addToast, ToastType, dismissToast } from '@standardnotes/toast' import { c } from 'ttag' @@ -12,16 +12,18 @@ export const downloadBlobOnAndroid = async ( filename: string, showToast = true, ) => { - const safeFilename = sanitizeFileNameForNativeWrite(filename) let loadingToastId: string | undefined if (showToast) { loadingToastId = addToast({ type: ToastType.Loading, - message: jtString(c('B8.MobileDesktopShared.Mobile.Info').jt`Downloading ${safeFilename}..`), + message: jtString(c('B8.MobileDesktopShared.Mobile.Info').jt`Downloading ${filename}..`), }) } const base64 = await getBase64FromBlob(blob) - const downloaded = await mobileDevice.downloadBase64AsFile(base64, safeFilename) + const { name, ext } = parseFileName(filename) + const sanitizedName = sanitizeFileName(name) + filename = `${sanitizedName}.${ext}` + const downloaded = await mobileDevice.downloadBase64AsFile(base64, filename) if (loadingToastId) { dismissToast(loadingToastId) } @@ -31,12 +33,12 @@ export const downloadBlobOnAndroid = async ( if (downloaded) { addToast({ type: ToastType.Success, - message: jtString(c('B8.MobileDesktopShared.Mobile.Info').jt`Downloaded ${safeFilename}`), + message: jtString(c('B8.MobileDesktopShared.Mobile.Info').jt`Downloaded ${filename}`), }) } else { addToast({ type: ToastType.Error, - message: jtString(c('B8.MobileDesktopShared.Mobile.Error').jt`Could not download ${safeFilename}`), + message: jtString(c('B8.MobileDesktopShared.Mobile.Error').jt`Could not download ${filename}`), }) } } From ecce747a3d16554208cd3cdac28984b2a8d7b89e Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Fri, 7 Aug 2026 15:27:33 -0300 Subject: [PATCH 3/3] chore: fix yarn.lock --- yarn.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 1f63f8302d1..705f5286875 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10001,7 +10001,6 @@ __metadata: "@standardnotes/config": ^2.4.3 "@standardnotes/react-native-utils": 1.0.1 "@standardnotes/snjs": "workspace:*" - "@standardnotes/utils": "workspace:*" "@standardnotes/web": "workspace:*" "@tsconfig/react-native": ^3.0.2 "@types/react": ^19.1.2