From a251a51cd85724a8b4225fff3f2c8614bc540067 Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Mon, 12 Jan 2026 15:30:14 -0300 Subject: [PATCH 01/11] fix: normalize url --- app/containers/UIKit/Image.tsx | 13 +++- .../__snapshots__/Markdown.test.tsx.snap | 8 +-- .../message/Components/Attachments/Reply.tsx | 14 +++- app/containers/message/Urls.tsx | 16 +++-- app/lib/methods/helpers/normalizeImageUrl.ts | 68 +++++++++++++++++++ 5 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 app/lib/methods/helpers/normalizeImageUrl.ts diff --git a/app/containers/UIKit/Image.tsx b/app/containers/UIKit/Image.tsx index bac03b64d3d..077a0861e55 100644 --- a/app/containers/UIKit/Image.tsx +++ b/app/containers/UIKit/Image.tsx @@ -5,6 +5,7 @@ import { BlockContext } from '@rocket.chat/ui-kit'; import ImageContainer from '../message/Components/Attachments/Image'; import Navigation from '../../lib/navigation/appNavigation'; +import { normalizeImageUrl } from '../../lib/methods/helpers/normalizeImageUrl'; import { type IThumb, type IImage, type IElement } from './interfaces'; import { type IAttachment } from '../../definitions'; @@ -23,9 +24,15 @@ const ThumbContext = (args: IThumb) => ( ); -export const Thumb = ({ element, size = 88 }: IThumb) => ( - -); +export const Thumb = ({ element, size = 88 }: IThumb) => { + const normalizedUrl = normalizeImageUrl(element?.imageUrl); + + if (!normalizedUrl) { + return null; + } + + return ; +}; export const Media = ({ element }: IImage) => { const showAttachment = (attachment: IAttachment) => Navigation.navigate('AttachmentView', { attachment }); diff --git a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap index 0ff288d776a..0f157bd9cfa 100644 --- a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap +++ b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap @@ -4407,7 +4407,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - 12:00 PM + 09:00 AM @@ -4481,7 +4481,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - 12:00:00 PM + 09:00:00 AM @@ -4703,7 +4703,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - Wednesday, Jan 01, 2025 12:00 PM + Wednesday, Jan 01, 2025 09:00 AM @@ -4777,7 +4777,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - Wednesday, Jan 01, 2025 12:00:00 PM + Wednesday, Jan 01, 2025 09:00:00 AM diff --git a/app/containers/message/Components/Attachments/Reply.tsx b/app/containers/message/Components/Attachments/Reply.tsx index eec13fe43c0..00203207af9 100644 --- a/app/containers/message/Components/Attachments/Reply.tsx +++ b/app/containers/message/Components/Attachments/Reply.tsx @@ -7,6 +7,7 @@ import { type IAttachment, type TGetCustomEmoji } from '../../../../definitions' import { themes } from '../../../../lib/constants/colors'; import { fileDownloadAndPreview } from '../../../../lib/methods/helpers'; import { formatAttachmentUrl } from '../../../../lib/methods/helpers/formatAttachmentUrl'; +import { normalizeImageUrl } from '../../../../lib/methods/helpers/normalizeImageUrl'; import openLink from '../../../../lib/methods/helpers/openLink'; import { type TSupportedThemes, useTheme } from '../../../../theme'; import sharedStyles from '../../../../views/Styles'; @@ -165,8 +166,17 @@ const UrlImage = React.memo( return null; } - image = image.includes('http') ? image : `${baseUrl}/${image}?rc_uid=${user.id}&rc_token=${user.token}`; - return ; + // Build the full URL if it's a relative path + const fullImageUrl = image.includes('http') ? image : `${baseUrl}/${image}?rc_uid=${user.id}&rc_token=${user.token}`; + + // Normalize the URL to ensure it's a valid HTTP/HTTPS URL + const normalizedUrl = normalizeImageUrl(fullImageUrl); + + if (!normalizedUrl) { + return null; + } + + return ; }, (prevProps, nextProps) => prevProps.image === nextProps.image ); diff --git a/app/containers/message/Urls.tsx b/app/containers/message/Urls.tsx index 717754f9c3b..da61e642d52 100644 --- a/app/containers/message/Urls.tsx +++ b/app/containers/message/Urls.tsx @@ -8,6 +8,7 @@ import axios from 'axios'; import { useAppSelector } from '../../lib/hooks/useAppSelector'; import Touchable from './Touchable'; import openLink from '../../lib/methods/helpers/openLink'; +import { normalizeImageUrl } from '../../lib/methods/helpers/normalizeImageUrl'; import sharedStyles from '../../views/Styles'; import { useTheme } from '../../theme'; import { LISTENER } from '../Toast'; @@ -68,9 +69,12 @@ const UrlImage = ({ image, hasContent }: { image: string; hasContent: boolean }) const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 }); const maxSize = useContext(WidthAwareContext); + // Normalize the URL to ensure it's a valid HTTP/HTTPS URL + const normalizedUrl = normalizeImageUrl(image); + useLayoutEffect(() => { - if (image && maxSize) { - Image.loadAsync(image, { + if (normalizedUrl && maxSize) { + Image.loadAsync(normalizedUrl, { onError: () => { setImageDimensions({ width: -1, height: -1 }); }, @@ -79,7 +83,11 @@ const UrlImage = ({ image, hasContent }: { image: string; hasContent: boolean }) setImageDimensions({ width: image.width, height: image.height }); }); } - }, [image, maxSize]); + }, [normalizedUrl, maxSize]); + + if (!normalizedUrl) { + return null; + } if (!imageDimensions.width || !imageDimensions.height) { return ; @@ -112,7 +120,7 @@ const UrlImage = ({ image, hasContent }: { image: string; hasContent: boolean }) return ( - + ); }; diff --git a/app/lib/methods/helpers/normalizeImageUrl.ts b/app/lib/methods/helpers/normalizeImageUrl.ts new file mode 100644 index 00000000000..6e0aa381305 --- /dev/null +++ b/app/lib/methods/helpers/normalizeImageUrl.ts @@ -0,0 +1,68 @@ +import { URL } from 'react-native-url-polyfill'; + +/** + * Normalizes and validates image URLs for use with expo-image. + * Ensures URLs are valid HTTP/HTTPS URLs and not file paths to prevent + * iOS crashes when expo-image's native code incorrectly uses URL(fileURLWithPath:) + * for HTTP URLs. + * + * @param url - The URL string to normalize + * @returns Normalized HTTP/HTTPS URL string or null if invalid + */ +export const normalizeImageUrl = (url: string | null | undefined): string | null => { + if (!url || typeof url !== 'string') { + return null; + } + + // Trim whitespace + const trimmedUrl = url.trim(); + + if (!trimmedUrl) { + return null; + } + + // Reject file:// URLs - these should be handled separately + if (trimmedUrl.startsWith('file://')) { + return null; + } + + // Reject data: URLs - these are handled differently by expo-image + if (trimmedUrl.startsWith('data:')) { + return null; + } + + try { + // Try to parse as URL to validate + const urlObj = new URL(trimmedUrl); + + // Only allow http and https protocols + if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') { + return null; + } + + // Return the normalized URL string + // This ensures proper encoding and formatting + return urlObj.toString(); + } catch (error) { + // If URL parsing fails, try to fix common issues + // Check if it's a relative URL that needs a protocol + if (trimmedUrl.startsWith('//')) { + // Protocol-relative URL - add https + try { + const urlObj = new URL(`https:${trimmedUrl}`); + return urlObj.toString(); + } catch { + return null; + } + } + + // If it starts with http but parsing failed, it might be malformed + // Return null to prevent crashes + if (trimmedUrl.toLowerCase().startsWith('http')) { + return null; + } + + // Not a valid URL + return null; + } +}; From 78fb02affe084eaf8de72e0872c38b3e3ba7a7c3 Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Mon, 12 Jan 2026 15:37:40 -0300 Subject: [PATCH 02/11] fix: snapshot --- .../markdown/__snapshots__/Markdown.test.tsx.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap index 0f157bd9cfa..0ff288d776a 100644 --- a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap +++ b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap @@ -4407,7 +4407,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - 09:00 AM + 12:00 PM @@ -4481,7 +4481,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - 09:00:00 AM + 12:00:00 PM @@ -4703,7 +4703,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - Wednesday, Jan 01, 2025 09:00 AM + Wednesday, Jan 01, 2025 12:00 PM @@ -4777,7 +4777,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - Wednesday, Jan 01, 2025 09:00:00 AM + Wednesday, Jan 01, 2025 12:00:00 PM From 6566c7b891dc80fa3f8952f6676cf3adad49e8d7 Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Mon, 12 Jan 2026 19:33:58 -0300 Subject: [PATCH 03/11] fix: expo url validation --- patches/expo-modules-core+2.3.12.patch | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 patches/expo-modules-core+2.3.12.patch diff --git a/patches/expo-modules-core+2.3.12.patch b/patches/expo-modules-core+2.3.12.patch new file mode 100644 index 00000000000..cd32118b729 --- /dev/null +++ b/patches/expo-modules-core+2.3.12.patch @@ -0,0 +1,83 @@ +diff --git a/node_modules/expo-modules-core/android/.project b/node_modules/expo-modules-core/android/.project +new file mode 100644 +index 0000000..dc11424 +--- /dev/null ++++ b/node_modules/expo-modules-core/android/.project +@@ -0,0 +1,28 @@ ++ ++ ++ expo-modules-core ++ Project expo-modules-core created by Buildship. ++ ++ ++ ++ ++ org.eclipse.buildship.core.gradleprojectbuilder ++ ++ ++ ++ ++ ++ org.eclipse.buildship.core.gradleprojectnature ++ ++ ++ ++ 1767968821913 ++ ++ 30 ++ ++ org.eclipse.core.resources.regexFilterMatcher ++ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ ++ ++ ++ ++ +diff --git a/node_modules/expo-modules-core/expo-module-gradle-plugin/.project b/node_modules/expo-modules-core/expo-module-gradle-plugin/.project +new file mode 100644 +index 0000000..483638f +--- /dev/null ++++ b/node_modules/expo-modules-core/expo-module-gradle-plugin/.project +@@ -0,0 +1,28 @@ ++ ++ ++ expo-module-gradle-plugin ++ Project expo-module-gradle-plugin created by Buildship. ++ ++ ++ ++ ++ org.eclipse.buildship.core.gradleprojectbuilder ++ ++ ++ ++ ++ ++ org.eclipse.buildship.core.gradleprojectnature ++ ++ ++ ++ 1767968821910 ++ ++ 30 ++ ++ org.eclipse.core.resources.regexFilterMatcher ++ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ ++ ++ ++ ++ +diff --git a/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift b/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift +index a126e11..6810dd0 100644 +--- a/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift ++++ b/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift +@@ -26,6 +26,10 @@ extension URL: Convertible { + + // File path doesn't need to be percent-encoded. + if isFileUrlPath(value) { ++ // Validate path is not empty to prevent crashes ++ guard !value.isEmpty else { ++ throw UrlContainsInvalidCharactersException() ++ } + return URL(fileURLWithPath: value) + } + From 2877737ee5039eaf9c28e8711407af7cc8773db4 Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Mon, 12 Jan 2026 20:41:16 -0300 Subject: [PATCH 04/11] improve image validation --- app/containers/ImageViewer/ImageViewer.tsx | 10 +++ .../Components/Attachments/Image/Image.tsx | 8 +- app/lib/methods/helpers/normalizeImageUrl.ts | 55 ++++++++++++ app/views/ShareView/Preview.tsx | 14 ++++ patches/expo-modules-core+2.3.12.patch | 83 ------------------- 5 files changed, 85 insertions(+), 85 deletions(-) delete mode 100644 patches/expo-modules-core+2.3.12.patch diff --git a/app/containers/ImageViewer/ImageViewer.tsx b/app/containers/ImageViewer/ImageViewer.tsx index 802597ed354..d4a2ff2d029 100644 --- a/app/containers/ImageViewer/ImageViewer.tsx +++ b/app/containers/ImageViewer/ImageViewer.tsx @@ -8,6 +8,7 @@ import Touch from '../Touch'; import { useUserPreferences } from '../../lib/methods/userPreferences'; import { AUTOPLAY_GIFS_PREFERENCES_KEY } from '../../lib/constants/keys'; import { useTheme } from '../../theme'; +import { isValidImageUri } from '../../lib/methods/helpers/normalizeImageUrl'; interface ImageViewerProps { style?: StyleProp; @@ -126,6 +127,15 @@ export const ImageViewer = ({ uri = '', width, height, ...props }: ImageViewerPr const { colors } = useTheme(); + // Validate URI before rendering to prevent crashes + if (!isValidImageUri(uri)) { + return ( + + + + ); + } + return ( diff --git a/app/containers/message/Components/Attachments/Image/Image.tsx b/app/containers/message/Components/Attachments/Image/Image.tsx index 74d43d0e84f..120e1dd1e5c 100644 --- a/app/containers/message/Components/Attachments/Image/Image.tsx +++ b/app/containers/message/Components/Attachments/Image/Image.tsx @@ -12,6 +12,7 @@ import { useUserPreferences } from '../../../../../lib/methods/userPreferences'; import { AUTOPLAY_GIFS_PREFERENCES_KEY } from '../../../../../lib/constants/keys'; import ImageBadge from './ImageBadge'; import log from '../../../../../lib/methods/helpers/log'; +import { isValidImageUri } from '../../../../../lib/methods/helpers/normalizeImageUrl'; export const MessageImage = React.memo(({ uri, status, encrypted = false, imagePreview, imageType }: IMessageImage) => { 'use memo'; @@ -24,7 +25,7 @@ export const MessageImage = React.memo(({ uri, status, encrypted = false, imageP const isGif = imageType === 'image/gif'; useEffect(() => { - if (status === 'downloaded') { + if (status === 'downloaded' && isValidImageUri(uri)) { Image.loadAsync(uri, { onError: e => { log(e); @@ -65,9 +66,12 @@ export const MessageImage = React.memo(({ uri, status, encrypted = false, imageP ); } + // Validate URI before rendering to prevent crashes + const isValidUri = isValidImageUri(uri); + return ( <> - {showImage ? ( + {showImage && isValidUri ? ( diff --git a/app/lib/methods/helpers/normalizeImageUrl.ts b/app/lib/methods/helpers/normalizeImageUrl.ts index 6e0aa381305..7eeff4856ee 100644 --- a/app/lib/methods/helpers/normalizeImageUrl.ts +++ b/app/lib/methods/helpers/normalizeImageUrl.ts @@ -1,5 +1,60 @@ import { URL } from 'react-native-url-polyfill'; +/** + * Validates if a URI is safe to use with expo-image. + * Prevents crashes by checking if the URI can be safely converted to a URL object. + * + * @param uri - The URI string to validate + * @returns true if the URI is safe to use, false otherwise + */ +export const isValidImageUri = (uri: string | null | undefined): boolean => { + if (!uri || typeof uri !== 'string') { + return false; + } + + // Trim whitespace + const trimmedUri = uri.trim(); + + // Empty strings will cause crashes in expo-modules-core's URL(fileURLWithPath:) + if (!trimmedUri) { + return false; + } + + // Data URIs are safe + if (trimmedUri.startsWith('data:')) { + return true; + } + + // File URIs need to be valid + if (trimmedUri.startsWith('file://')) { + try { + new URL(trimmedUri); + return true; + } catch { + return false; + } + } + + // HTTP/HTTPS URLs need to be valid + if (trimmedUri.startsWith('http://') || trimmedUri.startsWith('https://')) { + try { + new URL(trimmedUri); + return true; + } catch { + return false; + } + } + + // For absolute file paths (starting with /), check if they're not empty + // Empty paths will crash URL(fileURLWithPath:) + if (trimmedUri.startsWith('/')) { + return trimmedUri.length > 1; + } + + // Reject other formats that might cause issues + return false; +}; + /** * Normalizes and validates image URLs for use with expo-image. * Ensures URLs are valid HTTP/HTTPS URLs and not file paths to prevent diff --git a/app/views/ShareView/Preview.tsx b/app/views/ShareView/Preview.tsx index eae8e9f7805..dd9b9d854a9 100644 --- a/app/views/ShareView/Preview.tsx +++ b/app/views/ShareView/Preview.tsx @@ -13,6 +13,7 @@ import { THUMBS_HEIGHT } from './constants'; import { type TSupportedThemes } from '../../theme'; import { themes } from '../../lib/constants/colors'; import { type IShareAttachment } from '../../definitions'; +import { isValidImageUri } from '../../lib/methods/helpers/normalizeImageUrl'; const MESSAGE_COMPOSER_HEIGHT = 56; @@ -98,6 +99,19 @@ const Preview = React.memo(({ item, theme, length }: IPreview) => { } if (type?.match(/image/)) { + // Validate URI before rendering to prevent crashes + if (!isValidImageUri(item.path)) { + return ( + + ); + } const imageViewerWidth = width - insets.left - insets.right; return ; } diff --git a/patches/expo-modules-core+2.3.12.patch b/patches/expo-modules-core+2.3.12.patch deleted file mode 100644 index cd32118b729..00000000000 --- a/patches/expo-modules-core+2.3.12.patch +++ /dev/null @@ -1,83 +0,0 @@ -diff --git a/node_modules/expo-modules-core/android/.project b/node_modules/expo-modules-core/android/.project -new file mode 100644 -index 0000000..dc11424 ---- /dev/null -+++ b/node_modules/expo-modules-core/android/.project -@@ -0,0 +1,28 @@ -+ -+ -+ expo-modules-core -+ Project expo-modules-core created by Buildship. -+ -+ -+ -+ -+ org.eclipse.buildship.core.gradleprojectbuilder -+ -+ -+ -+ -+ -+ org.eclipse.buildship.core.gradleprojectnature -+ -+ -+ -+ 1767968821913 -+ -+ 30 -+ -+ org.eclipse.core.resources.regexFilterMatcher -+ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ -+ -+ -+ -+ -diff --git a/node_modules/expo-modules-core/expo-module-gradle-plugin/.project b/node_modules/expo-modules-core/expo-module-gradle-plugin/.project -new file mode 100644 -index 0000000..483638f ---- /dev/null -+++ b/node_modules/expo-modules-core/expo-module-gradle-plugin/.project -@@ -0,0 +1,28 @@ -+ -+ -+ expo-module-gradle-plugin -+ Project expo-module-gradle-plugin created by Buildship. -+ -+ -+ -+ -+ org.eclipse.buildship.core.gradleprojectbuilder -+ -+ -+ -+ -+ -+ org.eclipse.buildship.core.gradleprojectnature -+ -+ -+ -+ 1767968821910 -+ -+ 30 -+ -+ org.eclipse.core.resources.regexFilterMatcher -+ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ -+ -+ -+ -+ -diff --git a/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift b/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift -index a126e11..6810dd0 100644 ---- a/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift -+++ b/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift -@@ -26,6 +26,10 @@ extension URL: Convertible { - - // File path doesn't need to be percent-encoded. - if isFileUrlPath(value) { -+ // Validate path is not empty to prevent crashes -+ guard !value.isEmpty else { -+ throw UrlContainsInvalidCharactersException() -+ } - return URL(fileURLWithPath: value) - } - From 9fd30c044a2c4e14d93526ee41447f41558d639d Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Mon, 12 Jan 2026 20:42:07 -0300 Subject: [PATCH 05/11] fix: jest --- .../markdown/__snapshots__/Markdown.test.tsx.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap index 0ff288d776a..0f157bd9cfa 100644 --- a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap +++ b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap @@ -4407,7 +4407,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - 12:00 PM + 09:00 AM @@ -4481,7 +4481,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - 12:00:00 PM + 09:00:00 AM @@ -4703,7 +4703,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - Wednesday, Jan 01, 2025 12:00 PM + Wednesday, Jan 01, 2025 09:00 AM @@ -4777,7 +4777,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - Wednesday, Jan 01, 2025 12:00:00 PM + Wednesday, Jan 01, 2025 09:00:00 AM From 92ba31e49589ecc483471be53ccf62174f09709d Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Mon, 12 Jan 2026 21:15:05 -0300 Subject: [PATCH 06/11] fix: snapshot --- .../markdown/__snapshots__/Markdown.test.tsx.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap index 0f157bd9cfa..0ff288d776a 100644 --- a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap +++ b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap @@ -4407,7 +4407,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - 09:00 AM + 12:00 PM @@ -4481,7 +4481,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - 09:00:00 AM + 12:00:00 PM @@ -4703,7 +4703,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - Wednesday, Jan 01, 2025 09:00 AM + Wednesday, Jan 01, 2025 12:00 PM @@ -4777,7 +4777,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - Wednesday, Jan 01, 2025 09:00:00 AM + Wednesday, Jan 01, 2025 12:00:00 PM From e559e0832d9d4bbfc36030148f319c4dd148f06b Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Tue, 13 Jan 2026 11:38:03 -0300 Subject: [PATCH 07/11] patch to see logs --- patches/expo-modules-core+2.3.12.patch | 129 +++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 patches/expo-modules-core+2.3.12.patch diff --git a/patches/expo-modules-core+2.3.12.patch b/patches/expo-modules-core+2.3.12.patch new file mode 100644 index 00000000000..8c6e43acd5d --- /dev/null +++ b/patches/expo-modules-core+2.3.12.patch @@ -0,0 +1,129 @@ +diff --git a/node_modules/expo-modules-core/android/.project b/node_modules/expo-modules-core/android/.project +new file mode 100644 +index 0000000..dc11424 +--- /dev/null ++++ b/node_modules/expo-modules-core/android/.project +@@ -0,0 +1,28 @@ ++ ++ ++ expo-modules-core ++ Project expo-modules-core created by Buildship. ++ ++ ++ ++ ++ org.eclipse.buildship.core.gradleprojectbuilder ++ ++ ++ ++ ++ ++ org.eclipse.buildship.core.gradleprojectnature ++ ++ ++ ++ 1767968821913 ++ ++ 30 ++ ++ org.eclipse.core.resources.regexFilterMatcher ++ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ ++ ++ ++ ++ +diff --git a/node_modules/expo-modules-core/expo-module-gradle-plugin/.project b/node_modules/expo-modules-core/expo-module-gradle-plugin/.project +new file mode 100644 +index 0000000..483638f +--- /dev/null ++++ b/node_modules/expo-modules-core/expo-module-gradle-plugin/.project +@@ -0,0 +1,28 @@ ++ ++ ++ expo-module-gradle-plugin ++ Project expo-module-gradle-plugin created by Buildship. ++ ++ ++ ++ ++ org.eclipse.buildship.core.gradleprojectbuilder ++ ++ ++ ++ ++ ++ org.eclipse.buildship.core.gradleprojectnature ++ ++ ++ ++ 1767968821910 ++ ++ 30 ++ ++ org.eclipse.core.resources.regexFilterMatcher ++ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ ++ ++ ++ ++ +diff --git a/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift b/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift +index a126e11..b9a67af 100644 +--- a/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift ++++ b/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift +@@ -1,6 +1,7 @@ + // Copyright 2018-present 650 Industries. All rights reserved. + + import CoreGraphics ++import os.log + + // Here we extend some common iOS types to implement `Convertible` protocol and + // describe how they can be converted from primitive types received from JavaScript runtime. +@@ -19,22 +20,47 @@ extension URL: Convertible { + throw Conversions.ConvertingException(value) + } + ++ // Log the URI being converted for debugging crashes ++ let logger = OSLog(subsystem: "dev.expo.modules.core", category: "URLConversion") ++ os_log("🔍 [URL.convert] Attempting to convert URI: %{public}@ (length: %d)", log: logger, type: .default, value, value.count) ++ + // First we try to create a URL without extra encoding, as it came. + if let url = convertToUrl(string: value) { ++ os_log("✅ [URL.convert] Successfully converted via convertToUrl: %{public}@", log: logger, type: .default, value) + return url + } + + // File path doesn't need to be percent-encoded. + if isFileUrlPath(value) { +- return URL(fileURLWithPath: value) ++ os_log("📁 [URL.convert] Detected file path, using fileURLWithPath: %{public}@ (length: %d, isEmpty: %{public}@)", log: logger, type: .default, value, value.count, value.isEmpty ? "YES" : "NO") ++ ++ // Validate path is not empty to prevent crashes ++ guard !value.isEmpty else { ++ os_log("❌ [URL.convert] CRASH PREVENTION: Empty file path detected! Throwing exception instead of crashing.", log: logger, type: .error) ++ throw UrlContainsInvalidCharactersException() ++ } ++ ++ // Try to create the file URL and catch any assertion failures ++ // Note: URL(fileURLWithPath:) can crash with assertion failures for invalid paths ++ // We log extensively before this call to identify problematic URIs ++ do { ++ let fileURL = URL(fileURLWithPath: value) ++ os_log("✅ [URL.convert] Successfully created file URL: %{public}@", log: logger, type: .default, fileURL.absoluteString) ++ return fileURL ++ } catch { ++ os_log("❌ [URL.convert] EXCEPTION creating file URL from path: %{public}@ - Error: %{public}@", log: logger, type: .error, value, error.localizedDescription) ++ throw UrlContainsInvalidCharactersException() ++ } + } + + // If we get here, the string is not the file url and may require percent-encoding characters that are not URL-safe according to RFC 3986. + if let encodedValue = percentEncodeUrlString(value), let url = convertToUrl(string: encodedValue) { ++ os_log("✅ [URL.convert] Successfully converted after percent-encoding: %{public}@", log: logger, type: .default, value) + return url + } + + // If it still fails to create the URL object, the string possibly contains characters that must be explicitly percent-encoded beforehand. ++ os_log("❌ [URL.convert] FAILED to convert URI: %{public}@ (length: %d, isEmpty: %{public}@)", log: logger, type: .error, value, value.count, value.isEmpty ? "YES" : "NO") + throw UrlContainsInvalidCharactersException() + } + } From 7fb5bbb955f30eb0487d6999d3450d09010fcd0a Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Tue, 13 Jan 2026 12:59:23 -0300 Subject: [PATCH 08/11] try to fix exception --- patches/expo-modules-core+2.3.12.patch | 56 ++++++++++++++++++++------ 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/patches/expo-modules-core+2.3.12.patch b/patches/expo-modules-core+2.3.12.patch index 8c6e43acd5d..bcd149d3431 100644 --- a/patches/expo-modules-core+2.3.12.patch +++ b/patches/expo-modules-core+2.3.12.patch @@ -67,7 +67,7 @@ index 0000000..483638f + + diff --git a/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift b/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift -index a126e11..b9a67af 100644 +index a126e11..b050e10 100644 --- a/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift +++ b/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift @@ -1,6 +1,7 @@ @@ -78,7 +78,7 @@ index a126e11..b9a67af 100644 // Here we extend some common iOS types to implement `Convertible` protocol and // describe how they can be converted from primitive types received from JavaScript runtime. -@@ -19,22 +20,47 @@ extension URL: Convertible { +@@ -19,22 +20,79 @@ extension URL: Convertible { throw Conversions.ConvertingException(value) } @@ -93,27 +93,59 @@ index a126e11..b9a67af 100644 } // File path doesn't need to be percent-encoded. ++ // iOS 18.6+ CRITICAL: Has stricter validation for fileURLWithPath that causes assertion failures ++ // in production builds. We must validate extensively before calling it. if isFileUrlPath(value) { - return URL(fileURLWithPath: value) + os_log("📁 [URL.convert] Detected file path, using fileURLWithPath: %{public}@ (length: %d, isEmpty: %{public}@)", log: logger, type: .default, value, value.count, value.isEmpty ? "YES" : "NO") + -+ // Validate path is not empty to prevent crashes ++ // iOS 18.6+ CRITICAL: Empty paths will crash with assertion failure in production + guard !value.isEmpty else { + os_log("❌ [URL.convert] CRASH PREVENTION: Empty file path detected! Throwing exception instead of crashing.", log: logger, type: .error) + throw UrlContainsInvalidCharactersException() + } + -+ // Try to create the file URL and catch any assertion failures -+ // Note: URL(fileURLWithPath:) can crash with assertion failures for invalid paths -+ // We log extensively before this call to identify problematic URIs -+ do { -+ let fileURL = URL(fileURLWithPath: value) -+ os_log("✅ [URL.convert] Successfully created file URL: %{public}@", log: logger, type: .default, fileURL.absoluteString) -+ return fileURL -+ } catch { -+ os_log("❌ [URL.convert] EXCEPTION creating file URL from path: %{public}@ - Error: %{public}@", log: logger, type: .error, value, error.localizedDescription) ++ // iOS 18.6+ requires stricter validation - check for invalid characters ++ let trimmedPath = value.trimmingCharacters(in: .whitespacesAndNewlines) ++ guard !trimmedPath.isEmpty else { ++ os_log("❌ [URL.convert] CRASH PREVENTION: Path is empty after trimming: %{public}@", log: logger, type: .error, value) + throw UrlContainsInvalidCharactersException() + } ++ ++ // Validate path format for iOS 18.6+ strict requirements ++ // Must be absolute path (starts with /) or already a file:// URL ++ let isAbsolutePath = trimmedPath.hasPrefix("/") ++ let isFileURL = trimmedPath.hasPrefix("file://") ++ ++ guard isAbsolutePath || isFileURL else { ++ os_log("❌ [URL.convert] CRASH PREVENTION: Path is not absolute and not file:// URL (iOS 18.6 strict): %{public}@", log: logger, type: .error, value) ++ throw UrlContainsInvalidCharactersException() ++ } ++ ++ // Check for null characters and control characters that iOS 18.6 rejects ++ if trimmedPath.contains("\0") || trimmedPath.rangeOfCharacter(from: CharacterSet.controlCharacters) != nil { ++ os_log("❌ [URL.convert] CRASH PREVENTION: Path contains invalid control characters (iOS 18.6 strict): %{public}@", log: logger, type: .error, value) ++ throw UrlContainsInvalidCharactersException() ++ } ++ ++ // Create file URL - iOS 18.6 will assert if path is still invalid ++ // Note: We can't catch assertion failures, so validation above is critical ++ let fileURL: URL ++ if isFileURL { ++ // Already a file:// URL, parse it safely ++ guard let parsedURL = URL(string: trimmedPath) else { ++ os_log("❌ [URL.convert] CRASH PREVENTION: Invalid file:// URL format: %{public}@", log: logger, type: .error, value) ++ throw UrlContainsInvalidCharactersException() ++ } ++ fileURL = parsedURL ++ } else { ++ // Absolute path - iOS 18.6 will assert if malformed ++ // We've validated above, but if it still crashes, the logs will show the exact path ++ fileURL = URL(fileURLWithPath: trimmedPath) ++ } ++ ++ os_log("✅ [URL.convert] Successfully created file URL: %{public}@", log: logger, type: .default, fileURL.absoluteString) ++ return fileURL } // If we get here, the string is not the file url and may require percent-encoding characters that are not URL-safe according to RFC 3986. From 87968c387787bb2c2b1acbe349da083b99d70a30 Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Tue, 13 Jan 2026 13:17:41 -0300 Subject: [PATCH 09/11] fix: improve url validation --- patches/expo-modules-core+2.3.12.patch | 42 ++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/patches/expo-modules-core+2.3.12.patch b/patches/expo-modules-core+2.3.12.patch index bcd149d3431..7f24cace963 100644 --- a/patches/expo-modules-core+2.3.12.patch +++ b/patches/expo-modules-core+2.3.12.patch @@ -67,7 +67,7 @@ index 0000000..483638f + + diff --git a/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift b/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift -index a126e11..b050e10 100644 +index a126e11..b6d66e4 100644 --- a/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift +++ b/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift @@ -1,6 +1,7 @@ @@ -78,13 +78,37 @@ index a126e11..b050e10 100644 // Here we extend some common iOS types to implement `Convertible` protocol and // describe how they can be converted from primitive types received from JavaScript runtime. -@@ -19,22 +20,79 @@ extension URL: Convertible { +@@ -19,22 +20,117 @@ extension URL: Convertible { throw Conversions.ConvertingException(value) } + // Log the URI being converted for debugging crashes + let logger = OSLog(subsystem: "dev.expo.modules.core", category: "URLConversion") + os_log("🔍 [URL.convert] Attempting to convert URI: %{public}@ (length: %d)", log: logger, type: .default, value, value.count) ++ ++ // iOS 18.6+ CRITICAL: Check for HTTP/HTTPS URLs FIRST to prevent API misuse ++ // iOS 18.6 will crash with "API MISUSE: URL(filePath:) called with an HTTP URL string" ++ // if we try to use fileURLWithPath on an HTTP URL ++ let lowercasedValue = value.lowercased() ++ let isHttpUrl = lowercasedValue.hasPrefix("http://") || lowercasedValue.hasPrefix("https://") ++ ++ if isHttpUrl { ++ os_log("🌐 [URL.convert] Detected HTTP/HTTPS URL, using URL(string:): %{public}@", log: logger, type: .default, value) ++ // Try to parse as HTTP URL first ++ if let url = URL(string: value) { ++ os_log("✅ [URL.convert] Successfully converted HTTP URL: %{public}@", log: logger, type: .default, url.absoluteString) ++ return url ++ } ++ // If URL(string:) fails, try with URLComponents for better RFC 3986 support ++ if #available(iOS 16, *) { ++ if let url = URLComponents(string: value)?.url { ++ os_log("✅ [URL.convert] Successfully converted HTTP URL via URLComponents: %{public}@", log: logger, type: .default, url.absoluteString) ++ return url ++ } ++ } ++ os_log("❌ [URL.convert] Failed to parse HTTP URL: %{public}@", log: logger, type: .error, value) ++ throw UrlContainsInvalidCharactersException() ++ } + // First we try to create a URL without extra encoding, as it came. if let url = convertToUrl(string: value) { @@ -95,6 +119,7 @@ index a126e11..b050e10 100644 // File path doesn't need to be percent-encoded. + // iOS 18.6+ CRITICAL: Has stricter validation for fileURLWithPath that causes assertion failures + // in production builds. We must validate extensively before calling it. ++ // Only check for file paths if it's NOT an HTTP URL (already checked above) if isFileUrlPath(value) { - return URL(fileURLWithPath: value) + os_log("📁 [URL.convert] Detected file path, using fileURLWithPath: %{public}@ (length: %d, isEmpty: %{public}@)", log: logger, type: .default, value, value.count, value.isEmpty ? "YES" : "NO") @@ -114,8 +139,21 @@ index a126e11..b050e10 100644 + + // Validate path format for iOS 18.6+ strict requirements + // Must be absolute path (starts with /) or already a file:// URL ++ // CRITICAL: Double-check it's not an HTTP URL that slipped through + let isAbsolutePath = trimmedPath.hasPrefix("/") + let isFileURL = trimmedPath.hasPrefix("file://") ++ let trimmedLowercased = trimmedPath.lowercased() ++ let isHttpUrl = trimmedLowercased.hasPrefix("http://") || trimmedLowercased.hasPrefix("https://") ++ ++ // iOS 18.6+ will crash if we call fileURLWithPath on an HTTP URL ++ guard !isHttpUrl else { ++ os_log("❌ [URL.convert] CRASH PREVENTION: HTTP URL incorrectly identified as file path (iOS 18.6 strict): %{public}@", log: logger, type: .error, value) ++ // Try to parse as HTTP URL instead ++ if let url = URL(string: value) { ++ return url ++ } ++ throw UrlContainsInvalidCharactersException() ++ } + + guard isAbsolutePath || isFileURL else { + os_log("❌ [URL.convert] CRASH PREVENTION: Path is not absolute and not file:// URL (iOS 18.6 strict): %{public}@", log: logger, type: .error, value) From 306f81e319a12c501ed3eb7290fb944e5fe3177c Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Tue, 13 Jan 2026 17:47:17 -0300 Subject: [PATCH 10/11] chore: rollback unused code --- app/containers/ImageViewer/ImageViewer.tsx | 10 -- app/containers/UIKit/Image.tsx | 13 +- .../Components/Attachments/Image/Image.tsx | 8 +- .../message/Components/Attachments/Reply.tsx | 14 +- app/containers/message/Urls.tsx | 16 +-- app/lib/methods/helpers/normalizeImageUrl.ts | 123 ------------------ app/views/ShareView/Preview.tsx | 14 -- 7 files changed, 11 insertions(+), 187 deletions(-) delete mode 100644 app/lib/methods/helpers/normalizeImageUrl.ts diff --git a/app/containers/ImageViewer/ImageViewer.tsx b/app/containers/ImageViewer/ImageViewer.tsx index d4a2ff2d029..802597ed354 100644 --- a/app/containers/ImageViewer/ImageViewer.tsx +++ b/app/containers/ImageViewer/ImageViewer.tsx @@ -8,7 +8,6 @@ import Touch from '../Touch'; import { useUserPreferences } from '../../lib/methods/userPreferences'; import { AUTOPLAY_GIFS_PREFERENCES_KEY } from '../../lib/constants/keys'; import { useTheme } from '../../theme'; -import { isValidImageUri } from '../../lib/methods/helpers/normalizeImageUrl'; interface ImageViewerProps { style?: StyleProp; @@ -127,15 +126,6 @@ export const ImageViewer = ({ uri = '', width, height, ...props }: ImageViewerPr const { colors } = useTheme(); - // Validate URI before rendering to prevent crashes - if (!isValidImageUri(uri)) { - return ( - - - - ); - } - return ( diff --git a/app/containers/UIKit/Image.tsx b/app/containers/UIKit/Image.tsx index 077a0861e55..bac03b64d3d 100644 --- a/app/containers/UIKit/Image.tsx +++ b/app/containers/UIKit/Image.tsx @@ -5,7 +5,6 @@ import { BlockContext } from '@rocket.chat/ui-kit'; import ImageContainer from '../message/Components/Attachments/Image'; import Navigation from '../../lib/navigation/appNavigation'; -import { normalizeImageUrl } from '../../lib/methods/helpers/normalizeImageUrl'; import { type IThumb, type IImage, type IElement } from './interfaces'; import { type IAttachment } from '../../definitions'; @@ -24,15 +23,9 @@ const ThumbContext = (args: IThumb) => ( ); -export const Thumb = ({ element, size = 88 }: IThumb) => { - const normalizedUrl = normalizeImageUrl(element?.imageUrl); - - if (!normalizedUrl) { - return null; - } - - return ; -}; +export const Thumb = ({ element, size = 88 }: IThumb) => ( + +); export const Media = ({ element }: IImage) => { const showAttachment = (attachment: IAttachment) => Navigation.navigate('AttachmentView', { attachment }); diff --git a/app/containers/message/Components/Attachments/Image/Image.tsx b/app/containers/message/Components/Attachments/Image/Image.tsx index 120e1dd1e5c..74d43d0e84f 100644 --- a/app/containers/message/Components/Attachments/Image/Image.tsx +++ b/app/containers/message/Components/Attachments/Image/Image.tsx @@ -12,7 +12,6 @@ import { useUserPreferences } from '../../../../../lib/methods/userPreferences'; import { AUTOPLAY_GIFS_PREFERENCES_KEY } from '../../../../../lib/constants/keys'; import ImageBadge from './ImageBadge'; import log from '../../../../../lib/methods/helpers/log'; -import { isValidImageUri } from '../../../../../lib/methods/helpers/normalizeImageUrl'; export const MessageImage = React.memo(({ uri, status, encrypted = false, imagePreview, imageType }: IMessageImage) => { 'use memo'; @@ -25,7 +24,7 @@ export const MessageImage = React.memo(({ uri, status, encrypted = false, imageP const isGif = imageType === 'image/gif'; useEffect(() => { - if (status === 'downloaded' && isValidImageUri(uri)) { + if (status === 'downloaded') { Image.loadAsync(uri, { onError: e => { log(e); @@ -66,12 +65,9 @@ export const MessageImage = React.memo(({ uri, status, encrypted = false, imageP ); } - // Validate URI before rendering to prevent crashes - const isValidUri = isValidImageUri(uri); - return ( <> - {showImage && isValidUri ? ( + {showImage ? ( diff --git a/app/containers/message/Components/Attachments/Reply.tsx b/app/containers/message/Components/Attachments/Reply.tsx index 00203207af9..eec13fe43c0 100644 --- a/app/containers/message/Components/Attachments/Reply.tsx +++ b/app/containers/message/Components/Attachments/Reply.tsx @@ -7,7 +7,6 @@ import { type IAttachment, type TGetCustomEmoji } from '../../../../definitions' import { themes } from '../../../../lib/constants/colors'; import { fileDownloadAndPreview } from '../../../../lib/methods/helpers'; import { formatAttachmentUrl } from '../../../../lib/methods/helpers/formatAttachmentUrl'; -import { normalizeImageUrl } from '../../../../lib/methods/helpers/normalizeImageUrl'; import openLink from '../../../../lib/methods/helpers/openLink'; import { type TSupportedThemes, useTheme } from '../../../../theme'; import sharedStyles from '../../../../views/Styles'; @@ -166,17 +165,8 @@ const UrlImage = React.memo( return null; } - // Build the full URL if it's a relative path - const fullImageUrl = image.includes('http') ? image : `${baseUrl}/${image}?rc_uid=${user.id}&rc_token=${user.token}`; - - // Normalize the URL to ensure it's a valid HTTP/HTTPS URL - const normalizedUrl = normalizeImageUrl(fullImageUrl); - - if (!normalizedUrl) { - return null; - } - - return ; + image = image.includes('http') ? image : `${baseUrl}/${image}?rc_uid=${user.id}&rc_token=${user.token}`; + return ; }, (prevProps, nextProps) => prevProps.image === nextProps.image ); diff --git a/app/containers/message/Urls.tsx b/app/containers/message/Urls.tsx index da61e642d52..717754f9c3b 100644 --- a/app/containers/message/Urls.tsx +++ b/app/containers/message/Urls.tsx @@ -8,7 +8,6 @@ import axios from 'axios'; import { useAppSelector } from '../../lib/hooks/useAppSelector'; import Touchable from './Touchable'; import openLink from '../../lib/methods/helpers/openLink'; -import { normalizeImageUrl } from '../../lib/methods/helpers/normalizeImageUrl'; import sharedStyles from '../../views/Styles'; import { useTheme } from '../../theme'; import { LISTENER } from '../Toast'; @@ -69,12 +68,9 @@ const UrlImage = ({ image, hasContent }: { image: string; hasContent: boolean }) const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 }); const maxSize = useContext(WidthAwareContext); - // Normalize the URL to ensure it's a valid HTTP/HTTPS URL - const normalizedUrl = normalizeImageUrl(image); - useLayoutEffect(() => { - if (normalizedUrl && maxSize) { - Image.loadAsync(normalizedUrl, { + if (image && maxSize) { + Image.loadAsync(image, { onError: () => { setImageDimensions({ width: -1, height: -1 }); }, @@ -83,11 +79,7 @@ const UrlImage = ({ image, hasContent }: { image: string; hasContent: boolean }) setImageDimensions({ width: image.width, height: image.height }); }); } - }, [normalizedUrl, maxSize]); - - if (!normalizedUrl) { - return null; - } + }, [image, maxSize]); if (!imageDimensions.width || !imageDimensions.height) { return ; @@ -120,7 +112,7 @@ const UrlImage = ({ image, hasContent }: { image: string; hasContent: boolean }) return ( - + ); }; diff --git a/app/lib/methods/helpers/normalizeImageUrl.ts b/app/lib/methods/helpers/normalizeImageUrl.ts deleted file mode 100644 index 7eeff4856ee..00000000000 --- a/app/lib/methods/helpers/normalizeImageUrl.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { URL } from 'react-native-url-polyfill'; - -/** - * Validates if a URI is safe to use with expo-image. - * Prevents crashes by checking if the URI can be safely converted to a URL object. - * - * @param uri - The URI string to validate - * @returns true if the URI is safe to use, false otherwise - */ -export const isValidImageUri = (uri: string | null | undefined): boolean => { - if (!uri || typeof uri !== 'string') { - return false; - } - - // Trim whitespace - const trimmedUri = uri.trim(); - - // Empty strings will cause crashes in expo-modules-core's URL(fileURLWithPath:) - if (!trimmedUri) { - return false; - } - - // Data URIs are safe - if (trimmedUri.startsWith('data:')) { - return true; - } - - // File URIs need to be valid - if (trimmedUri.startsWith('file://')) { - try { - new URL(trimmedUri); - return true; - } catch { - return false; - } - } - - // HTTP/HTTPS URLs need to be valid - if (trimmedUri.startsWith('http://') || trimmedUri.startsWith('https://')) { - try { - new URL(trimmedUri); - return true; - } catch { - return false; - } - } - - // For absolute file paths (starting with /), check if they're not empty - // Empty paths will crash URL(fileURLWithPath:) - if (trimmedUri.startsWith('/')) { - return trimmedUri.length > 1; - } - - // Reject other formats that might cause issues - return false; -}; - -/** - * Normalizes and validates image URLs for use with expo-image. - * Ensures URLs are valid HTTP/HTTPS URLs and not file paths to prevent - * iOS crashes when expo-image's native code incorrectly uses URL(fileURLWithPath:) - * for HTTP URLs. - * - * @param url - The URL string to normalize - * @returns Normalized HTTP/HTTPS URL string or null if invalid - */ -export const normalizeImageUrl = (url: string | null | undefined): string | null => { - if (!url || typeof url !== 'string') { - return null; - } - - // Trim whitespace - const trimmedUrl = url.trim(); - - if (!trimmedUrl) { - return null; - } - - // Reject file:// URLs - these should be handled separately - if (trimmedUrl.startsWith('file://')) { - return null; - } - - // Reject data: URLs - these are handled differently by expo-image - if (trimmedUrl.startsWith('data:')) { - return null; - } - - try { - // Try to parse as URL to validate - const urlObj = new URL(trimmedUrl); - - // Only allow http and https protocols - if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') { - return null; - } - - // Return the normalized URL string - // This ensures proper encoding and formatting - return urlObj.toString(); - } catch (error) { - // If URL parsing fails, try to fix common issues - // Check if it's a relative URL that needs a protocol - if (trimmedUrl.startsWith('//')) { - // Protocol-relative URL - add https - try { - const urlObj = new URL(`https:${trimmedUrl}`); - return urlObj.toString(); - } catch { - return null; - } - } - - // If it starts with http but parsing failed, it might be malformed - // Return null to prevent crashes - if (trimmedUrl.toLowerCase().startsWith('http')) { - return null; - } - - // Not a valid URL - return null; - } -}; diff --git a/app/views/ShareView/Preview.tsx b/app/views/ShareView/Preview.tsx index dd9b9d854a9..eae8e9f7805 100644 --- a/app/views/ShareView/Preview.tsx +++ b/app/views/ShareView/Preview.tsx @@ -13,7 +13,6 @@ import { THUMBS_HEIGHT } from './constants'; import { type TSupportedThemes } from '../../theme'; import { themes } from '../../lib/constants/colors'; import { type IShareAttachment } from '../../definitions'; -import { isValidImageUri } from '../../lib/methods/helpers/normalizeImageUrl'; const MESSAGE_COMPOSER_HEIGHT = 56; @@ -99,19 +98,6 @@ const Preview = React.memo(({ item, theme, length }: IPreview) => { } if (type?.match(/image/)) { - // Validate URI before rendering to prevent crashes - if (!isValidImageUri(item.path)) { - return ( - - ); - } const imageViewerWidth = width - insets.left - insets.right; return ; } From 4d0d0d23c65c66189a5dcb2f32061c1c275eef8b Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Tue, 13 Jan 2026 17:52:00 -0300 Subject: [PATCH 11/11] chore: cleanup --- patches/expo-modules-core+2.3.12.patch | 109 +------------------------ 1 file changed, 2 insertions(+), 107 deletions(-) diff --git a/patches/expo-modules-core+2.3.12.patch b/patches/expo-modules-core+2.3.12.patch index 7f24cace963..4d880777722 100644 --- a/patches/expo-modules-core+2.3.12.patch +++ b/patches/expo-modules-core+2.3.12.patch @@ -1,91 +1,11 @@ -diff --git a/node_modules/expo-modules-core/android/.project b/node_modules/expo-modules-core/android/.project -new file mode 100644 -index 0000000..dc11424 ---- /dev/null -+++ b/node_modules/expo-modules-core/android/.project -@@ -0,0 +1,28 @@ -+ -+ -+ expo-modules-core -+ Project expo-modules-core created by Buildship. -+ -+ -+ -+ -+ org.eclipse.buildship.core.gradleprojectbuilder -+ -+ -+ -+ -+ -+ org.eclipse.buildship.core.gradleprojectnature -+ -+ -+ -+ 1767968821913 -+ -+ 30 -+ -+ org.eclipse.core.resources.regexFilterMatcher -+ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ -+ -+ -+ -+ -diff --git a/node_modules/expo-modules-core/expo-module-gradle-plugin/.project b/node_modules/expo-modules-core/expo-module-gradle-plugin/.project -new file mode 100644 -index 0000000..483638f ---- /dev/null -+++ b/node_modules/expo-modules-core/expo-module-gradle-plugin/.project -@@ -0,0 +1,28 @@ -+ -+ -+ expo-module-gradle-plugin -+ Project expo-module-gradle-plugin created by Buildship. -+ -+ -+ -+ -+ org.eclipse.buildship.core.gradleprojectbuilder -+ -+ -+ -+ -+ -+ org.eclipse.buildship.core.gradleprojectnature -+ -+ -+ -+ 1767968821910 -+ -+ 30 -+ -+ org.eclipse.core.resources.regexFilterMatcher -+ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ -+ -+ -+ -+ diff --git a/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift b/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift -index a126e11..b6d66e4 100644 +index a126e11..35d620c 100644 --- a/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift +++ b/node_modules/expo-modules-core/ios/Core/Arguments/Convertibles.swift -@@ -1,6 +1,7 @@ - // Copyright 2018-present 650 Industries. All rights reserved. - - import CoreGraphics -+import os.log - - // Here we extend some common iOS types to implement `Convertible` protocol and - // describe how they can be converted from primitive types received from JavaScript runtime. -@@ -19,22 +20,117 @@ extension URL: Convertible { +@@ -19,14 +19,88 @@ extension URL: Convertible { throw Conversions.ConvertingException(value) } -+ // Log the URI being converted for debugging crashes -+ let logger = OSLog(subsystem: "dev.expo.modules.core", category: "URLConversion") -+ os_log("🔍 [URL.convert] Attempting to convert URI: %{public}@ (length: %d)", log: logger, type: .default, value, value.count) -+ + // iOS 18.6+ CRITICAL: Check for HTTP/HTTPS URLs FIRST to prevent API misuse + // iOS 18.6 will crash with "API MISUSE: URL(filePath:) called with an HTTP URL string" + // if we try to use fileURLWithPath on an HTTP URL @@ -93,26 +13,21 @@ index a126e11..b6d66e4 100644 + let isHttpUrl = lowercasedValue.hasPrefix("http://") || lowercasedValue.hasPrefix("https://") + + if isHttpUrl { -+ os_log("🌐 [URL.convert] Detected HTTP/HTTPS URL, using URL(string:): %{public}@", log: logger, type: .default, value) + // Try to parse as HTTP URL first + if let url = URL(string: value) { -+ os_log("✅ [URL.convert] Successfully converted HTTP URL: %{public}@", log: logger, type: .default, url.absoluteString) + return url + } + // If URL(string:) fails, try with URLComponents for better RFC 3986 support + if #available(iOS 16, *) { + if let url = URLComponents(string: value)?.url { -+ os_log("✅ [URL.convert] Successfully converted HTTP URL via URLComponents: %{public}@", log: logger, type: .default, url.absoluteString) + return url + } + } -+ os_log("❌ [URL.convert] Failed to parse HTTP URL: %{public}@", log: logger, type: .error, value) + throw UrlContainsInvalidCharactersException() + } + // First we try to create a URL without extra encoding, as it came. if let url = convertToUrl(string: value) { -+ os_log("✅ [URL.convert] Successfully converted via convertToUrl: %{public}@", log: logger, type: .default, value) return url } @@ -122,18 +37,14 @@ index a126e11..b6d66e4 100644 + // Only check for file paths if it's NOT an HTTP URL (already checked above) if isFileUrlPath(value) { - return URL(fileURLWithPath: value) -+ os_log("📁 [URL.convert] Detected file path, using fileURLWithPath: %{public}@ (length: %d, isEmpty: %{public}@)", log: logger, type: .default, value, value.count, value.isEmpty ? "YES" : "NO") -+ + // iOS 18.6+ CRITICAL: Empty paths will crash with assertion failure in production + guard !value.isEmpty else { -+ os_log("❌ [URL.convert] CRASH PREVENTION: Empty file path detected! Throwing exception instead of crashing.", log: logger, type: .error) + throw UrlContainsInvalidCharactersException() + } + + // iOS 18.6+ requires stricter validation - check for invalid characters + let trimmedPath = value.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedPath.isEmpty else { -+ os_log("❌ [URL.convert] CRASH PREVENTION: Path is empty after trimming: %{public}@", log: logger, type: .error, value) + throw UrlContainsInvalidCharactersException() + } + @@ -147,7 +58,6 @@ index a126e11..b6d66e4 100644 + + // iOS 18.6+ will crash if we call fileURLWithPath on an HTTP URL + guard !isHttpUrl else { -+ os_log("❌ [URL.convert] CRASH PREVENTION: HTTP URL incorrectly identified as file path (iOS 18.6 strict): %{public}@", log: logger, type: .error, value) + // Try to parse as HTTP URL instead + if let url = URL(string: value) { + return url @@ -156,13 +66,11 @@ index a126e11..b6d66e4 100644 + } + + guard isAbsolutePath || isFileURL else { -+ os_log("❌ [URL.convert] CRASH PREVENTION: Path is not absolute and not file:// URL (iOS 18.6 strict): %{public}@", log: logger, type: .error, value) + throw UrlContainsInvalidCharactersException() + } + + // Check for null characters and control characters that iOS 18.6 rejects + if trimmedPath.contains("\0") || trimmedPath.rangeOfCharacter(from: CharacterSet.controlCharacters) != nil { -+ os_log("❌ [URL.convert] CRASH PREVENTION: Path contains invalid control characters (iOS 18.6 strict): %{public}@", log: logger, type: .error, value) + throw UrlContainsInvalidCharactersException() + } + @@ -172,28 +80,15 @@ index a126e11..b6d66e4 100644 + if isFileURL { + // Already a file:// URL, parse it safely + guard let parsedURL = URL(string: trimmedPath) else { -+ os_log("❌ [URL.convert] CRASH PREVENTION: Invalid file:// URL format: %{public}@", log: logger, type: .error, value) + throw UrlContainsInvalidCharactersException() + } + fileURL = parsedURL + } else { + // Absolute path - iOS 18.6 will assert if malformed -+ // We've validated above, but if it still crashes, the logs will show the exact path + fileURL = URL(fileURLWithPath: trimmedPath) + } + -+ os_log("✅ [URL.convert] Successfully created file URL: %{public}@", log: logger, type: .default, fileURL.absoluteString) + return fileURL } // If we get here, the string is not the file url and may require percent-encoding characters that are not URL-safe according to RFC 3986. - if let encodedValue = percentEncodeUrlString(value), let url = convertToUrl(string: encodedValue) { -+ os_log("✅ [URL.convert] Successfully converted after percent-encoding: %{public}@", log: logger, type: .default, value) - return url - } - - // If it still fails to create the URL object, the string possibly contains characters that must be explicitly percent-encoded beforehand. -+ os_log("❌ [URL.convert] FAILED to convert URI: %{public}@ (length: %d, isEmpty: %{public}@)", log: logger, type: .error, value, value.count, value.isEmpty ? "YES" : "NO") - throw UrlContainsInvalidCharactersException() - } - }